Xcode|为 App 添加更换 Icon 图标的功能

理解如何在 Xcode 为 iOS App 增加更换图标功能,为你的 App 进一步提升吸引力。

在 Xcode 中进行配置

启用 Include all app icon assets 选项

在 WWDC21 之前,你需要手动在 Info.plist 中填写所有备用图标的名称。

从 Xcode 13 开始,可以通过在 Xcode 中启用 Include all app icon assets,在编译时可以自动添加到 Info.plist。

添加备用图标文件

从 Xcode 26 开始,苹果推荐使用新的 Icon Composer 文件(.icns),而不再使用之前在 Assets 这种添加的图标文件。

图标同样必须以 AppIcon 命名,可以直接添加到 Xcode 项目根路径。

仍然需要格外为每个图标添加一个 Preview 文件。

创建图标选择视图组件

核心是通过调用 UIApplication.shared.setAlternateIconName(name) 组件,iOS 系统即可自动完成图标的切换。

以下组件创建了一个基于 Grid 布局的图标切换视图:

import Observation
import SwiftUI

@Observable
final class AppInfoProvider {
    private static func getValue<Value>(for key: String) -> Value {
        guard
            let value = Bundle.main.infoDictionary?[key] as? Value
        else {
            fatalError("Missing value for \(key) in Info.plist")
        }
        return value
    }

    private static func getPrimaryAppIconName() -> String {
        let appIconsDict: [String: [String: Any]] = getValue(
            for: "CFBundleIcons")
        let primaryIconDict = appIconsDict["CFBundlePrimaryIcon"]

        guard
            let primaryIconName = primaryIconDict?["CFBundleIconName"]
                as? String
        else {
            fatalError("Missing primary icon name")
        }

        return primaryIconName
    }

    private static func getAlternateAppIconNames() -> [String] {
        let appIconsDict: [String: [String: Any]] = getValue(
            for: "CFBundleIcons")
        let alternateIconsDict =
            appIconsDict["CFBundleAlternateIcons"]
            as? [String: [String: String]]

        var alternateAppIconNames = [String]()
        alternateIconsDict?.forEach { _, value in
            if let alternateIconName = value["CFBundleIconName"] {
                alternateAppIconNames.append(alternateIconName)
            }
        }

        return alternateAppIconNames
    }

    let bundleDisplayName: String
    let bundleVersion: String
    let bundleShortVersionString: String
    let primaryAppIconName: String
    let alternateAppIconNames: [String]

    init() {
        bundleDisplayName = Self.getValue(for: "CFBundleDisplayName")
        bundleVersion = Self.getValue(for: "CFBundleVersion")
        bundleShortVersionString = Self.getValue(
            for: "CFBundleShortVersionString")
        primaryAppIconName = Self.getPrimaryAppIconName()
        alternateAppIconNames = Self.getAlternateAppIconNames()
    }
}

struct AppIconSettingsView: View {
    @State private var appInfoProvider = AppInfoProvider()
    @State private var selectedIcon: String?
    @State private var showErrorAlert = false
    @State private var errorMessage = ""

    // 定义网格列:自适应宽度,最小宽度为 100 点
    private let columns = [
        GridItem(.adaptive(minimum: 100), spacing: 20),
        GridItem(.adaptive(minimum: 100), spacing: 20),
        GridItem(.adaptive(minimum: 100), spacing: 20),
    ]

    var body: some View {
        LazyVGrid(columns: columns, spacing: 20) {
            iconGridItem(name: appInfoProvider.primaryAppIconName)

            ForEach(appInfoProvider.alternateAppIconNames, id: \.self) {
                iconName in
                iconGridItem(name: iconName)
            }
        }
        .alert("更换图标失败", isPresented: $showErrorAlert) {
            Button("确定", role: .cancel) {}
        } message: {
            Text(errorMessage)
        }
        .onAppear {
            selectedIcon = UIApplication.shared.alternateIconName
        }
    }

    // 网格项的视图:包含图标、名称和选中边框
    private func iconGridItem(name: String) -> some View {
        VStack(spacing: 8) {
            Button(action: {
                changeAppIcon(to: name)
                #if os(iOS)
                    UIImpactFeedbackGenerator(style: .light).impactOccurred()
                #endif
            }) {
                ZStack {
                    if name
                        == (selectedIcon ?? appInfoProvider.primaryAppIconName)
                    {
                        RoundedRectangle(cornerRadius: 12)
                            .stroke(Color.accentColor, lineWidth: 2)
                            .frame(width: 76, height: 76)
                    }

                    Image(uiImage: UIImage(named: name) ?? UIImage())
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 60, height: 60)
                        .cornerRadius(60 * 0.205)
                }
            }
            .buttonStyle(BorderlessButtonStyle())  // 防止 List 的点击效果影响按钮
            .frame(width: 76, height: 76)
        }
        .frame(width: 100, height: 100)
        // 第一层阴影 - 整体柔和的阴影
        .shadow(
            color: .black.opacity(0.1),
            radius: 8,
            x: 0,
            y: 4
        )

    }

    // 更改应用图标的函数
    private func changeAppIcon(to iconName: String) {
        let name =
            iconName == appInfoProvider.primaryAppIconName ? nil : iconName
        selectedIcon = name

        UIApplication.shared.setAlternateIconName(name) { error in
            if let error = error {
                DispatchQueue.main.async {
                    selectedIcon = UIApplication.shared.alternateIconName
                    errorMessage = error.localizedDescription
                    showErrorAlert = true
                }
            }
        }
    }
}

#Preview {
    NavigationStack {
        AppIconSettingsView()
    }
}

使用 PersonalizationSettingsView 组件

我已经创建一个通用的 PersonalizationSettingsView 组件,包含图标切换、强调色切换、ColorScheme 以及字体。

此处,记录如何使用它们。

自定义 ColorScheme 和强调色

首先,在 App 文件中添加以下变量


// 用于获取颜色模式
@AppStorage("appTheme") private var appearance: Appearance = .system
// 用于获取强调色
@AppStorage("accentColor") private var accentColor: String = "pink"

private var colorScheme: ColorScheme? {
    switch appearance {
    case .system: return nil
    case .light: return .light
    case .dark: return .dark
    }
}

private var actualAccentColor: Color {
    AccentColorSettingsView.AccentColor.getColor(from: accentColor)
}

在 App文件的 ContentView 中,添加以下修饰器:

WindowGroup {
    ContentView()
      .preferredColorScheme(colorScheme)
      .tint(actualAccentColor)  // 使用计算属性
}