Asia/Shanghai
March 5, 2026

iOS Alternate App Icons in Capacitor: 4 Failures, 1 Root Cause

Capacitor iOS 动态切换 App 图标:4 次失败,1 个根因

Mingjian Shao
iOS Alternate App Icons in Capacitor: 4 Failures, 1 Root Cause
I spent days trying to make iOS alternate app icons work in a Capacitor app. Four different approaches, four failures — all caused by a single Xcode build setting nobody talks about.
My fitness app FitFlow has three themes: green (default), orange, and citrus. When users switch themes, the home screen icon should match. iOS supports this via UIApplication.setAlternateIconName — straightforward in native Swift, but surprisingly painful in Capacitor.Following early Stack Overflow answers, I added files like AppIcon-Orange-120.png and AppIcon-Orange-180.png, referenced them in Info.plist under CFBundleAlternateIcons. Built, ran — blank icon. The API call succeeded (no error), but the home screen showed a white square.Renamed to OrangeIcon@2x.png (120×120) and OrangeIcon@3x.png (180×180) — the standard iOS asset naming convention. Updated Info.plist to use the base name OrangeIcon. Same result: blank icon.Created a proper OrangeIcon.appiconset with Contents.json in the asset catalog, just like the primary icon. Used 1024×1024 source images. Xcode compiled without warnings. Still blank.Found a pattern from Chinese dev blogs and Gemini suggesting simple filenames without size suffixes, with UIPrerenderedIcon set. Created clean PNGs, updated Info.plist. Same blank icon.At this point I'd tried every combination of naming, sizing, and catalog structure that the internet recommended. Something deeper was wrong.After attempt 4, I checked the compiled Info.plist inside the built .app bundle:
Bash
plutil -p .../Build/Products/Debug-iphoneos/App.app/Info.plist \
  | grep -A 30 CFBundleIcons
The CFBundleAlternateIcons key was completely missing from the compiled plist, even though it existed in the source Info.plist. Xcode was overwriting it at build time.The culprit: a build setting in project.pbxproj:
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES
When this setting is enabled, Xcode's asset catalog compiler takes over CFBundleIcons entirely. It scans the asset catalog, generates the icon configuration, and silently discards any manual CFBundleAlternateIcons entries from your source Info.plist. No warnings, no errors — just gone.In project.pbxproj, delete this line from both Debug and Release configurations:
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
Or in Xcode: Build Settings → search "Include All App Icon Assets" → delete the value.Place PNGs directly in the Xcode project (not in an asset catalog):
Text
ios/App/App/
├── OrangeIcon@2x.png      # 120×120, RGB, no alpha
├── OrangeIcon@3x.png      # 180×180, RGB, no alpha
├── OrangeLightIcon@2x.png  # 120×120, RGB, no alpha
└── OrangeLightIcon@3x.png  # 180×180, RGB, no alpha
Critical: PNGs must be RGB mode with no alpha channel. RGBA causes a black background on the icon change confirmation dialog.
Xml
<key>CFBundleIcons</key>
<dict>
    <key>CFBundleAlternateIcons</key>
    <dict>
        <key>OrangeIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
        <key>OrangeLightIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeLightIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
    </dict>
</dict>
<!-- iPad support -->
<key>CFBundleIcons~ipad</key>
<dict>
    <key>CFBundleAlternateIcons</key>
    <dict>
        <key>OrangeIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
        <key>OrangeLightIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeLightIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
    </dict>
</dict>
Use the base name only (no @2x/@3x suffix). iOS resolves the correct variant automatically.The PNGs must be registered in project.pbxproj — just dropping files in the directory isn't enough. Add them as:
  • PBXFileReference (file exists)
  • PBXBuildFile (included in build)
  • PBXGroup (visible in project navigator)
  • PBXResourcesBuildPhase (copied to app bundle)
Create a minimal native plugin to bridge the iOS API:
Swift
// AppIconPlugin.swift
import Capacitor
import UIKit

@objc(AppIconPlugin)
public class AppIconPlugin: CAPPlugin, CAPBridgedPlugin {
    public let identifier = "AppIconPlugin"
    public let jsName = "AppIcon"
    public let pluginMethods: [CAPPluginMethod] = [
        CAPPluginMethod(name: "setIcon", returnType: CAPPluginReturnPromise)
    ]

    @objc func setIcon(_ call: CAPPluginCall) {
        let iconName = call.getString("name")
        DispatchQueue.main.async {
            UIApplication.shared.setAlternateIconName(iconName) { error in
                if let error = error {
                    call.reject("Failed: \(error.localizedDescription)")
                } else {
                    call.resolve(["success": true])
                }
            }
        }
    }
}
Register it via a custom CAPBridgeViewController subclass:
Swift
// MyViewController.swift
import UIKit
import Capacitor

class MyViewController: CAPBridgeViewController {
    override open func capacitorDidLoad() {
        bridge?.registerPluginInstance(AppIconPlugin())
    }
}
Update Main.storyboard to use MyViewController as the custom class.
Typescript
import { registerPlugin } from "@capacitor/core";

interface AppIconPlugin {
  setIcon(options: { name: string | null }): Promise<{ success: boolean }>;
}
const AppIcon = registerPlugin<AppIconPlugin>("AppIcon");

// Map themes to icon names
const THEME_ICON_MAP: Record<string, string | null> = {
  green: null,           // null = primary icon (asset catalog)
  orange: "OrangeIcon",
  "orange-light": "OrangeLightIcon",
};

// When user switches theme:
await AppIcon.setIcon({ name: THEME_ICON_MAP[theme] });
Passing null resets to the primary icon from the asset catalog.Always check the compiled plist, not the source:
Bash
plutil -p .../App.app/Info.plist | grep -A 30 CFBundleIcons
If you see CFBundleAlternateIcons with your icon names listed, you're good. If it's missing, the asset catalog compiler is still overwriting it.
  • Check the compiled output, not the source. The source Info.plist can be perfect while the built app has completely different content.
  • ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS is the silent killer. It's enabled by default in many Xcode project templates. It makes the asset catalog compiler take full ownership of icon configuration.
  • Keep the primary icon in the asset catalog. Only alternate icons need to be loose PNGs. Xcode merges CFBundlePrimaryIcon from the asset catalog with your manual CFBundleAlternateIcons.
  • Use RGB PNGs, not RGBA. Alpha channels cause black backgrounds in the icon switch confirmation dialog.
  • No Capacitor community plugin needed. A 20-line native plugin is simpler and more reliable than depending on a third-party package.
If you use an AI coding assistant like OpenCode, paste this prompt to set up alternate icons for your Capacitor iOS app:
Markdown
I want to add alternate app icons to my Capacitor iOS app that switch with themes.
Reference: https://mjshao.fun/blog/capacitor-ios-alternate-icons

My setup:
- Capacitor version: [ask me]
- Number of alternate icons: [ask me]
- Icon names and theme mapping: [ask me]
- Icon PNG files ready? [ask me, need 120x120 @2x and 180x180 @3x, RGB no alpha]

Steps:
1. Remove ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS from project.pbxproj
2. Add icon PNGs to Xcode project (PBXFileReference + PBXBuildFile + PBXGroup + PBXResourcesBuildPhase)
3. Add CFBundleAlternateIcons to Info.plist (+ CFBundleIcons~ipad)
4. Create AppIconPlugin.swift with setAlternateIconName bridge
5. Create MyViewController.swift extending CAPBridgeViewController to register plugin
6. Update Main.storyboard to use MyViewController
7. Add TypeScript plugin registration and theme→icon mapping
8. Build and verify compiled Info.plist contains alternate icons
在 Capacitor 应用里实现 iOS 动态图标切换,试了 4 种方案全失败——最后发现是 Xcode 一个没人提的构建设置在悄悄搞鬼。
我的健身 App FitFlow 有三个主题:绿色(默认)、橙色、柑橘色。切换主题时,桌面图标也要跟着变。iOS 有 UIApplication.setAlternateIconName 这个 API——原生 Swift 很简单,但在 Capacitor 里踩了无数坑。按早期 Stack Overflow 的回答,加了 AppIcon-Orange-120.pngAppIcon-Orange-180.png,在 Info.plist 的 CFBundleAlternateIcons 里引用。编译运行——空白图标。API 调用没报错,但桌面显示白色方块。改成 OrangeIcon@2x.png(120×120)和 OrangeIcon@3x.png(180×180)——标准的 iOS 资源命名规范。Info.plist 用 base name OrangeIcon。结果一样:空白图标在 Asset Catalog 里建了正规的 OrangeIcon.appiconset,写好 Contents.json,用 1024×1024 源图。Xcode 编译没警告。还是空白。从中文开发博客和 Gemini 找到一种方案,用不带尺寸后缀的简单文件名,加上 UIPrerenderedIcon。做了干净的 PNG,更新 Info.plist。照样空白图标。试遍了网上所有命名、尺寸、目录结构的组合。问题一定出在更深的地方。第 4 次失败后,我检查了构建出来的 .app 包里编译后的 Info.plist:
Bash
plutil -p .../Build/Products/Debug-iphoneos/App.app/Info.plist \
  | grep -A 30 CFBundleIcons
CFBundleAlternateIcons 在编译后的 plist 里完全消失了,尽管源 Info.plist 里明明有。Xcode 在构建时把它覆盖了。罪魁祸首是 project.pbxproj 里的一个构建设置:
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES
这个设置开启后,Xcode 的 Asset Catalog 编译器会完全接管 CFBundleIcons。它扫描 Asset Catalog,自动生成图标配置,然后悄悄丢弃你在源 Info.plist 里手写的 CFBundleAlternateIcons。没有警告,没有报错——直接消失。project.pbxproj 的 Debug 和 Release 配置里,删掉这一行:
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
或者在 Xcode 里:Build Settings → 搜索 "Include All App Icon Assets" → 删掉值。把图标 PNG 直接放在 Xcode 项目里(不要放在 Asset Catalog 里):
Text
ios/App/App/
├── OrangeIcon@2x.png      # 120×120, RGB, 无 alpha
├── OrangeIcon@3x.png      # 180×180, RGB, 无 alpha
├── OrangeLightIcon@2x.png  # 120×120, RGB, 无 alpha
└── OrangeLightIcon@3x.png  # 180×180, RGB, 无 alpha
关键:PNG 必须是 RGB 模式,不能有 alpha 通道。RGBA 会导致图标切换确认弹窗里出现黑色背景。
Xml
<key>CFBundleIcons</key>
<dict>
    <key>CFBundleAlternateIcons</key>
    <dict>
        <key>OrangeIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
        <key>OrangeLightIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeLightIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
    </dict>
</dict>
<!-- iPad 支持 -->
<key>CFBundleIcons~ipad</key>
<dict>
    <key>CFBundleAlternateIcons</key>
    <dict>
        <key>OrangeIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
        <key>OrangeLightIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>OrangeLightIcon</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
    </dict>
</dict>
只用 base name(不带 @2x/@3x 后缀),iOS 会自动找对应分辨率的变体。光把文件丢到目录里不够,必须在 project.pbxproj 里注册:
  • PBXFileReference(文件存在)
  • PBXBuildFile(参与编译)
  • PBXGroup(项目导航器可见)
  • PBXResourcesBuildPhase(复制到 app bundle)
一个 20 行的原生插件就够了:
Swift
// AppIconPlugin.swift
import Capacitor
import UIKit

@objc(AppIconPlugin)
public class AppIconPlugin: CAPPlugin, CAPBridgedPlugin {
    public let identifier = "AppIconPlugin"
    public let jsName = "AppIcon"
    public let pluginMethods: [CAPPluginMethod] = [
        CAPPluginMethod(name: "setIcon", returnType: CAPPluginReturnPromise)
    ]

    @objc func setIcon(_ call: CAPPluginCall) {
        let iconName = call.getString("name")
        DispatchQueue.main.async {
            UIApplication.shared.setAlternateIconName(iconName) { error in
                if let error = error {
                    call.reject("Failed: \(error.localizedDescription)")
                } else {
                    call.resolve(["success": true])
                }
            }
        }
    }
}
通过自定义 CAPBridgeViewController 子类注册:
Swift
// MyViewController.swift
import UIKit
import Capacitor

class MyViewController: CAPBridgeViewController {
    override open func capacitorDidLoad() {
        bridge?.registerPluginInstance(AppIconPlugin())
    }
}
更新 Main.storyboard 使用 MyViewController
Typescript
import { registerPlugin } from "@capacitor/core";

interface AppIconPlugin {
  setIcon(options: { name: string | null }): Promise<{ success: boolean }>;
}
const AppIcon = registerPlugin<AppIconPlugin>("AppIcon");

const THEME_ICON_MAP: Record<string, string | null> = {
  green: null,           // null = 主图标(Asset Catalog 里的)
  orange: "OrangeIcon",
  "orange-light": "OrangeLightIcon",
};

// 切换主题时:
await AppIcon.setIcon({ name: THEME_ICON_MAP[theme] });
null 会重置回 Asset Catalog 里的主图标。一定要检查编译后的 plist,不是源文件:
Bash
plutil -p .../App.app/Info.plist | grep -A 30 CFBundleIcons
能看到 CFBundleAlternateIcons 带着你的图标名就对了。如果没有,说明 Asset Catalog 编译器还在覆盖。
  • 看编译产物,别看源文件。 源 Info.plist 可以完美无缺,但编译出来的 app 里内容完全不同。
  • ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS 是隐形杀手。 很多 Xcode 项目模板默认开启,它让 Asset Catalog 编译器完全接管图标配置。
  • 主图标留在 Asset Catalog 里。 只有备选图标需要用松散 PNG。Xcode 会自动把 Asset Catalog 的 CFBundlePrimaryIcon 和你手动写的 CFBundleAlternateIcons 合并。
  • 用 RGB PNG,不要 RGBA。 Alpha 通道会导致图标切换确认弹窗出现黑色背景。
  • 不需要社区插件。 20 行原生插件比依赖第三方包更简单可靠。
如果你用 AI 编程助手(OpenCode、Claude Code、Cursor 等),直接粘贴这段 prompt:
Markdown
帮我给 Capacitor iOS 应用添加主题联动的备选图标。
参考:https://mjshao.fun/blog/capacitor-ios-alternate-icons

我的项目:
- Capacitor 版本:[问我]
- 备选图标数量:[问我]
- 图标名称和主题映射:[问我]
- 图标 PNG 准备好了吗?[问我,需要 120x120 @2x 和 180x180 @3x,RGB 无 alpha]

执行步骤:
1. 从 project.pbxproj 删除 ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS
2. 把图标 PNG 加到 Xcode 项目(PBXFileReference + PBXBuildFile + PBXGroup + PBXResourcesBuildPhase)
3. 在 Info.plist 添加 CFBundleAlternateIcons(加上 CFBundleIcons~ipad)
4. 创建 AppIconPlugin.swift,桥接 setAlternateIconName
5. 创建 MyViewController.swift 继承 CAPBridgeViewController 注册插件
6. 更新 Main.storyboard 使用 MyViewController
7. 添加 TypeScript 插件注册和主题→图标映射
8. 构建并验证编译后的 Info.plist 包含备选图标
Share this post:
Enjoy this post? Subscribe via RSS: English | 中文