在 Emitron 项目上看到一个 App Icon 切换的功用,本文将探索并实现该功用。

在 iOS 上实现用户主动触发的 App Icon 切换

Colourful Demo

新建 SwiftUI 项目,就叫它 Colourful 吧~

在 iOS 上实现用户主动触发的 App Icon 切换

在 iOS 上实现用户主动触发的 App Icon 切换

在 ./Colorful/Colorful 文件加下,新增 App Icons 文件夹。借用一下 Emitron 的图标,将这些图标加入到 App Icons 文件夹中。每一种图标供给四张图片,分别是-ipad@2x、-ipadpro@2x、@2x、@3x。

在 iOS 上实现用户主动触发的 App Icon 切换

在 iOS 上实现用户主动触发的 App Icon 切换

CFBundleIcons

在 info 中 增加 Icon files (iOS 5) 字段:

在 iOS 上实现用户主动触发的 App Icon 切换

右击Icon files (iOS 5) ,勾选 Raw Keys and Values。将列出原始 Key 称号,而不是展现英文本地化字符串。能够看到原始 Key 为 CFBundleIcons。

在 iOS 上实现用户主动触发的 App Icon 切换

在 iOS 上实现用户主动触发的 App Icon 切换

Newsstand

Newsstand 是 Apple 在 iOS5 推出的寄存报刊杂志类内容的 App。在 iOS9 之后,苹果删去了这个 App,而 CFBundleIcons 下的 UINewsstandIcon 是服务于 Newsstand 的,因而咱们能够删去

UINewsstandIcon 这个 Key。

CFBundlePrimaryIcon

另一个 Key CFBundlePrimaryIcon,用来设置 App 的首要图标。这里需求留意,如果咱们已经在Assets.xcassets中,存在 AppIcon,那么CFBundlePrimaryIcon中的装备将会被忽略,Assets.xcassets的 AppIcon 将会自动装备到 CFBundlePrimaryIcon 中。

在 iOS 上实现用户主动触发的 App Icon 切换

  • UIPrerenderedIcon 是一个布尔值,指示图标文件是否已包括光泽作用,若为 NO,Apple 会为 App 在 AppStore 和 iTunes 上展现的 icon 增加光泽。

  • CFBundleIconName 表明应用程序图标的 asset 的称号。在 iOS 11 及更高版别通过输入 assets z中的称号进行捆绑,代表应用程序图标。如果您使用此键,您还应该在非 iOS 体系(如装备器和 MDM 解决方案)中包括至少一项,CFBundleIconFiles以便显现该图标。

  • CFBundleIconFiles 是图标文件的称号。如果面向 iOS 10 或更早版别,则是必需的字段。数组中的每个字符串都包括图标文件的称号。咱们能够包括多个不同大小的图标,以支撑 iPhone、iPad 和通用应用程序。

咱们能够删去 assets 中的 AppIcon,同时删去 Colorful Target 下 General Tag 下的 App Icons and Launch Screen 的 AppIcon 相关内容。

在 iOS 上实现用户主动触发的 App Icon 切换

在 iOS 上实现用户主动触发的 App Icon 切换

删去 CFBundleIconName ,并将 CFBundleIconFiles 的 item0 的值设置为图片称号 app-icon–default,来指定图标。运转项目,Colourful 的图标即被替换为对应的图标。

在 iOS 上实现用户主动触发的 App Icon 切换

在 iOS 上实现用户主动触发的 App Icon 切换

CFBundleAlternateIcons

此 Key 标识 App 的备用图标,需求咱们手动增加。

在 iOS 上实现用户主动触发的 App Icon 切换

UINewsstandBindingType、UINewsstandBindingEdge 如上文咱们并不需求,手动进行删去。而光泽作用 UIPrerenderedIcon,需求咱们手动增加。而 Emitron 的作用是多张 App Icon,因而,咱们需求对 CFBundleAlternateIcons 的结构进行调整。依据 Apple 文档,在 iOS 中,CFBundleAlternateIcons 的值是一个字典。每个字典条目的键是备用图标的称号。依据咱们的备用图标 black-white、white-black、multi-black、black-multi,咱们调整结构如下:

在 iOS 上实现用户主动触发的 App Icon 切换

CFBundleAlternateIcons 下有四个图标,每个图标有一个标识序号的 ordinal 字段,以及 UIPrerenderedIcon 和 CFBundleIconFiles 字段。

Colourful App

新增文件

新建文件 Icon.swift,表明图标:

import UIKit
struct Icon: Identifiable {
    var id: String { imageName }
    let ordinal: Int
    let name: String?
    let imageName: String
    var image: UIImage {
        .init(named: imageName) ?? .init()
    }
}
extension Icon: Comparable {
    static func < (lhs: Icon, rhs: Icon) -> Bool {
        lhs.ordinal < rhs.ordinal
    }
}

新建文件 IconManager.swift,它将处理图标的读取和更改,后续将持续完善:

import UIKit
import Combine
final class IconManager: ObservableObject {
    static let shared = IconManager()
    let icons: [Icon]
    @Published private(set) var currentIcon: Icon?
    init() {
        self.icons = []
        // Todo
    }
}

新增 View+Extension.swift,增加 ViewBuilder 注解的一个快捷办法:

import SwiftUI
extension View {
    @ViewBuilder func `if`<T: View>(_ conditional: Bool, transform: (Self) -> T) -> some View {
        if conditional {
            transform(self)
        } else {
            self
        }
    }
}

新增 IconView.swift 文件,画出图标,这里用到了 .if

import SwiftUI
struct IconView: View {
    let icon: Icon
    let selected: Bool
    var body: some View {
        Image(uiImage: icon.image)
          .renderingMode(.original)
          .cornerRadius(10)
          .overlay(
            RoundedRectangle(cornerRadius: 10)
              .stroke(lineWidth: 2)
          )
          .padding([.trailing], 2)
          .if(selected) {
            $0.overlay(
              Image(systemName: "checkmark.circle.fill")
                .font(.system(size: 20, weight: .bold))
                .foregroundColor(.green),
              alignment: .bottomTrailing
            )
          }
    }
}
struct IconView_Previews: PreviewProvider {
    static let darkIcon = Icon(ordinal: 0, name: nil, imageName: "app-icon--default")
    static let lightIcon = Icon(ordinal: 0, name: "black-white", imageName: "app-icon--black-white")
    static var previews: some View {
        HStack {
          IconView(icon: darkIcon, selected: false)
          IconView(icon: darkIcon, selected: true)
          IconView(icon: lightIcon, selected: false)
          IconView(icon: lightIcon, selected: true)
        }
    }
}

在 iOS 上实现用户主动触发的 App Icon 切换

新增 IconChooserView.swift,后续将展现可供更换的图标:

struct IconChooserView: View {
    @StateObject var iconManager = IconManager.shared
    var body: some View {
        HStack {
            ForEach(iconManager.icons) { icon in
                Button {
                    // Todo
                } label: {
                    IconView(icon: icon, selected: iconManager.currentIcon == icon)
                }
            }
        }
    }
}

新增 SettingsView.swift,放置 IconChooserView:

import SwiftUI
struct SettingsView: View {
    var body: some View {
        VStack {
            Section(
                header: HStack {
                    Text("App Icon")
                        .font(.title)
                        .bold()
                    Spacer()
                }
            ) {
                IconChooserView()
            }
        }
        .padding()
    }
}

调整 ContentView.swift,展现 SettingsView:

import SwiftUI
struct ContentView: View {
    var body: some View {
        VStack {
            SettingsView()
        }
    }
}

调整 IconManager

调整 IconManager 的 init 办法:

init() {
    let currentIconName = UIApplication.shared.alternateIconName
    self.icons = {
        guard let plistIcons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any] else {
            return  []
        }
        var icons: [Icon] = []
        // 增加首要图标
        if let primaryIcon = plistIcons["CFBundlePrimaryIcon"] as? [String: Any],
           let files = primaryIcon["CFBundleIconFiles"] as? [String],
           let fileName = files.first {
            icons.append(Icon(ordinal: 0, name: nil, imageName: fileName))
        }
        // 增加备用图标
        if let alternateIcons = plistIcons["CFBundleAlternateIcons"] as? [String: Any] {
            icons += alternateIcons.compactMap { key, value in
            guard let alternateIcon = value as? [String: Any],
              let files = alternateIcon["CFBundleIconFiles"] as? [String],
              let fileName = files.first,
              let ordinal = alternateIcon["ordinal"] as? Int else {
                return nil
            }
                return Icon(ordinal: ordinal, name: key, imageName: fileName)
          }
          .sorted()
        }
        return icons
    }()
    currentIcon = icons.first { $0.name == currentIconName }
}

这里先获取了当时图标名,由于咱们的 Primary Icon 没有姓名,所以 currentIconName 为空。icons 为首要图标和备用图标组成的数组。currentIcon 为当时的 Primary Icon。

运转程序,查看运转情况:

在 iOS 上实现用户主动触发的 App Icon 切换

持续新增代码,完结 set 办法:

extension IconManager {
    @MainActor func set(icon: Icon) async throws {
        do {
            try await UIApplication.shared.setAlternateIconName(icon.name)
            currentIcon = icon
        } catch {
            throw error
        }
    }
}

调整 IconChooserView

修改代码,弥补 Button 事情:

struct IconChooserView: View {
    @StateObject var iconManager = IconManager.shared
    var body: some View {
        HStack {
            ForEach(iconManager.icons) { icon in
                Button {
                    Task {
                        try await iconManager.set(icon: icon)
                    }
                } label: {
                    IconView(icon: icon, selected: iconManager.currentIcon == icon)
                }
            }
        }
    }
}

运转项目,测验更改图标,咱们的项目就完结啦~

能够从这里获取项目的源码