A subscriber upgrades and the app icon transforms into something special. A holiday icon shows up on the home screen during a limited window. Dark-mode lovers pick a moody black-and-gold version themselves. I wanted to ship one of these flourishes in a small personal app I built with Rork — and spent half a day staring at Expo SDK docs before realizing the answer is not in expo-image or expo-router at all.
iOS does provide a first-party Alternate App Icons API for exactly this. The trouble is, Rork (which sits on top of Expo) does not surface it through any managed-workflow setting. You have to hand-edit Info.plist, place assets in the right place, and wire up a community native module. Get any one of those wrong and the build either silently ignores the call or gets rejected by App Store review. Here is what worked for me, plus the three pitfalls I want to save you from.
Themed Icons and Alternate App Icons are not the same thing
The first hour of my detour came from confusing two features that sound similar but are completely different APIs.
- Themed Icons (iOS 18.0+): light, dark, and tinted variants the OS picks automatically. You add an
appearancesblock toAssets.xcassets/AppIcon.appiconset/Contents.jsonand you are done. Users cannot pick manually, and the icon adapts to the system appearance the user chose - Alternate App Icons (iOS 10.3+): explicit switches you trigger from code with
UIApplication.setAlternateIconName(_:). Independent of Themed Icons, and the user (or your app's logic) decides when the change happens (see the Themed App Icons walkthrough if that is what you actually need)
I started out wanting "a dark-mode-only icon," which Themed Icons handles by itself. The moment the requirement became "swap to a gold icon when the user starts a subscription," I needed Alternate Icons. Phrasing the requirement in one sentence — who triggers the swap, and when — tells you which API you actually need. If the answer is "the operating system, automatically," Themed Icons is enough. If the answer involves your in-app subscription state, a date condition, or a user preference toggle, you are looking at Alternate App Icons territory.
Three steps to enable Alternate App Icons in a Rork project
Apple's docs assume you are in Xcode editing the project file directly. Rork's managed workflow does not let you do that. Here is the minimum setup that worked for me, end to end.
Step 1: Drop the alternate icon PNGs into the project
Alternate icons are not part of the AppIcon.appiconset. They sit at the project root (or in a folder you explicitly include in your build) as plain PNGs referenced by filename. The minimum sizes you need:
[name]@2x.png(120×120)[name]@3x.png(180×180)- For iPad, also
[name]~ipad.png(76×76) and[name]~ipad@2x.png(152×152)
I initially nested mine in an AppIcon-Premium.appiconset catalog because that felt tidy. The build succeeded, setAlternateIconName returned Image not found, and I lost an hour staring at the Info.plist trying to figure out what was wrong. Alternate icons must live outside the asset catalog as standalone files. This is one of those iOS quirks that has been around since 2017 but rarely shows up in tutorials.
Step 2: Add CFBundleAlternateIcons to Info.plist
In Rork, you do this through the infoPlist block in app.json (or app.config.ts). Here is the structure that finally worked:
{
"expo": {
"ios": {
"infoPlist": {
"CFBundleIcons": {
"CFBundlePrimaryIcon": {
"CFBundleIconFiles": ["AppIcon"],
"UIPrerenderedIcon": false
},
"CFBundleAlternateIcons": {
"Premium": {
"CFBundleIconFiles": ["icon-premium"],
"UIPrerenderedIcon": false
},
"DarkGold": {
"CFBundleIconFiles": ["icon-dark-gold"],
"UIPrerenderedIcon": false
}
}
}
}
}
}
}The CFBundleIconFiles value is the base filename from Step 1 — no @2x or @3x suffix. iOS resolves the resolution variants at runtime based on device density. If you ship iPad, you do not need a separate iPad block here; iOS picks the ~ipad files by suffix.
The keys (Premium, DarkGold in this example) are what you will pass to setAlternateIconName later. Keep them human-readable — VoiceOver will read these aloud when blind users browse the home screen.
Step 3: Call the native function from JavaScript
Expo does not yet ship a first-party module for this, so the fastest path is the community-maintained expo-dynamic-app-icon. Install it with npx expo install expo-dynamic-app-icon and rebuild your Dev Client.
// app/components/IconSwitcher.tsx
import { setAppIcon, getAppIcon } from "expo-dynamic-app-icon";
import { Alert } from "react-native";
export async function switchToPremiumIcon() {
const current = getAppIcon();
if (current === "Premium") return; // already switched, do nothing
const result = await setAppIcon("Premium");
// result is "Premium" on success, or false on failure
if (result === false) {
Alert.alert("Icon switch failed", "Check your Info.plist setup.");
return;
}
// iOS will now show its own confirmation alert — you cannot suppress it
}
// Expected output:
// success → returns "Premium"; the home screen icon updates
// failure → returns false; the most likely cause is a missing Info.plist entryTo revert to the primary icon, call setAppIcon(null). The library wraps Apple's API faithfully, so the same rules apply: passing null resets to whatever you registered as CFBundlePrimaryIcon.
Three pitfalls that will eat your afternoon
Pitfall 1: Expo Go cannot run this
expo-dynamic-app-icon is a native module, so Expo Go throws setAppIcon is not a function. You cannot validate locally without building a custom Dev Client (I wrote up the Dev Client setup separately if you have not built one before). I ran eas build --profile development --platform ios and only then could I test the swap on a real device. Plan for the EAS build time in your schedule — for a small project, the first build can take 15 to 25 minutes, and incremental rebuilds when you add a new icon name to Info.plist take roughly the same time because Info.plist changes invalidate the cached prebuild.
Pitfall 2: You cannot suppress iOS's confirmation alert
Calling setAlternateIconName triggers a system alert: "This app's icon has changed." You cannot disable it through public APIs. There are private-API workarounds floating around online (swizzling presentViewController:animated:completion: on UIApplication, for example), but App Store review will reject them. I tried one of them as an experiment and the binary was flagged within 48 hours.
The cleaner solution is to show your own message first — something like "Your app icon will change. Take a peek at your home screen." — so the user is not surprised by the system dialog that follows. In practice, two confirmations in a row feel less jarring than a single unexpected one because the user has already mentally committed by the time iOS speaks up.
Pitfall 3: Icon cache makes "it didn't work" look real when it actually did
You add a new alternate icon, build, run, call the API, and nothing changes. This is iOS's SpringBoard icon cache, not your code (see also the icon-update cache troubleshooting notes). What worked for me on the simulator: delete the app, clean build, reinstall. On a physical device, also uninstall before re-testing. Skip this and you will doubt your Info.plist for an hour over a caching artifact.
If you suspect your code is right but the icon will not refresh on the simulator, you can also try xcrun simctl shutdown all followed by xcrun simctl erase all to nuke every simulator's state. It is heavy-handed, but it removes the variable.
Accessibility and review notes I wish I'd known earlier
Two things bit me only after I shipped the first version.
First, App Store Connect review checks every alternate icon — not just the primary — for brand alignment and trademark conflicts. A "dark mode" icon that accidentally resembles another company's logo will get the whole submission rejected under guideline 4.5.1. If you are commissioning artwork from a designer, send them a list of all icons going into the build and ask them to flag anything they think looks too close to a known brand. The reviewer will be doing the same check.
Second, alternate icons do not inherit accessibility labels from the primary icon. VoiceOver reads out the filename you registered. So icon-premium-v2-final is what blind users will hear when scanning their home screen. Use names that mean something to a person, like Premium or Classic. This is a small detail, but it is the kind of polish that separates a thoughtful app from a quickly-shipped one.
When the feature is and is not worth shipping
Before you spend a day on this, it helps to ask whether the feature actually moves the needle for your app. From small launches I have shipped and watched, alternate icons tend to have the most impact in three scenarios: rewarding a high-value action like a subscription upgrade, marking a seasonal event with a clear time window, and giving heavy users a sense of personalization. They tend to underperform when the trigger is too subtle (the user does not notice the change) or when the icons differ only slightly (you might as well have shipped Themed Icons and saved the work).
Where to start
If you are about to add this feature, the safest first move is to build a Dev Client through EAS and confirm a swap between two placeholder icons — say a red one and a blue one — before you commission real artwork. The behavior is finicky enough that you want to lock down the plumbing before any final design is touched. Once that round trip works, replacing the placeholders with the real artwork is the easy part.
Thanks for reading — I hope this saves you the afternoon I lost.