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.
What I Wanted
The 4 Attempts That Failed
Attempt 1: Loose PNGs with Size Suffixes
Attempt 2: Loose PNGs with @2x/@3x
Attempt 3: Asset Catalog .appiconset
Attempt 4: CSDN/Gemini Pattern (Simple Names)
The Root Cause
Bash
plutil -p .../Build/Products/Debug-iphoneos/App.app/Info.plist \
| grep -A 30 CFBundleIcons
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES
The Fix
1. Remove the Build Setting
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
2. Add Icon PNGs as Loose Files
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
3. Configure Info.plist
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>
4. Add PNGs to Xcode Project
- PBXFileReference (file exists)
- PBXBuildFile (included in build)
- PBXGroup (visible in project navigator)
- PBXResourcesBuildPhase (copied to app bundle)
5. Write a Capacitor Plugin
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])
}
}
}
}
}
Swift
// MyViewController.swift
import UIKit
import Capacitor
class MyViewController: CAPBridgeViewController {
override open func capacitorDidLoad() {
bridge?.registerPluginInstance(AppIconPlugin())
}
}
6. Call from TypeScript
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] });
Verification
Bash
plutil -p .../App.app/Info.plist | grep -A 30 CFBundleIcons
Key Takeaways
- 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.
Let AI Do It
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 一个没人提的构建设置在悄悄搞鬼。
需求
4 次失败的尝试
第 1 次:带尺寸后缀的 PNG
第 2 次:@2x/@3x 命名
第 3 次:Asset Catalog .appiconset
第 4 次:CSDN/Gemini 方案(简单文件名)
根因
Bash
plutil -p .../Build/Products/Debug-iphoneos/App.app/Info.plist \
| grep -A 30 CFBundleIcons
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES
解决方案
1. 删除构建设置
Text
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
2. 用松散 PNG 文件
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
3. 配置 Info.plist
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>
4. 在 Xcode 项目中注册 PNG
- PBXFileReference(文件存在)
- PBXBuildFile(参与编译)
- PBXGroup(项目导航器可见)
- PBXResourcesBuildPhase(复制到 app bundle)
5. 写 Capacitor 插件
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])
}
}
}
}
}
Swift
// MyViewController.swift
import UIKit
import Capacitor
class MyViewController: CAPBridgeViewController {
override open func capacitorDidLoad() {
bridge?.registerPluginInstance(AppIconPlugin())
}
}
6. TypeScript 调用
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] });
验证方法
Bash
plutil -p .../App.app/Info.plist | grep -A 30 CFBundleIcons
核心经验
- 看编译产物,别看源文件。 源 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 帮你搞定
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 包含备选图标