See an App Icon switching function on the Emitron project , this article will explore and implement this function.
Colourful Demo
Create a new SwiftUI project, let’s call it Colorful~
Add the App Icons folder under the ./Colorful/Colorful file. Borrow the icons from Emitron and add them to the App Icons folder. Each icon provides four pictures, namely -ipad@2x, -ipadpro@2x, @2x, @3x.
CFBundleIcons
Add Icon files (iOS 5) field in info:
Right-click Icon files (iOS 5) and check Raw Keys and Values. The original key name will be listed instead of displaying the English localized string. You can see that the original Key is CFBundleIcons.
Newsstand
Newsstand is an App launched by Apple on iOS5 to store newspaper and magazine content. After iOS9, Apple deleted this App, and the UINewsstandIcon under CFBundleIcons is for Newsstand, so we can delete it
The Key of UINewsstandIcon.
CFBundlePrimaryIcon
另一个 Key CFBundlePrimaryIcon,用来设置 App 的主要图标。这里需要注意,如果我们已经在Assets.xcassets中,存在 AppIcon,那么CFBundlePrimaryIcon中的配置将会被忽略,Assets.xcassets的 AppIcon 将会自动配置到 CFBundlePrimaryIcon 中。
-
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 相关内容。
删除 CFBundleIconName ,并将 CFBundleIconFiles 的 item0 的值设置为图片名称 app-icon--default,来指定图标。运行项目,Colourful 的图标即被替换为对应的图标。
CFBundleAlternateIcons
此 Key 标识 App 的备用图标,需要我们手动添加。
UINewsstandBindingType、UINewsstandBindingEdge 如上文我们并不需要,手动进行删除。而光泽效果 UIPrerenderedIcon,需要我们手动添加。而 Emitron 的效果是多张 App Icon,因此,我们需要对 CFBundleAlternateIcons 的结构进行调整。根据 Apple 文档,在 iOS 中,CFBundleAlternateIcons 的值是一个字典。每个字典条目的键是备用图标的名称。根据我们的备用图标 black-white、white-black、multi-black、black-multi,我们调整结构如下:
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)
}
}
}
复制代码
新增 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 }
}
复制代码
Here, the current icon name is obtained first. Since our Primary Icon has no name, currentIconName is empty. icons is an array of primary and alternate icons. currentIcon is the current Primary Icon.
Run the program to see how it works:
Continue to add code to complete the set method:
extension IconManager {
@MainActor func set(icon: Icon) async throws {
do {
try await UIApplication.shared.setAlternateIconName(icon.name)
currentIcon = icon
} catch {
throw error
}
}
}
复制代码
Adjust IconChooserView
Modify the code to add the Button event:
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)
}
}
}
}
}
复制代码
Run the project, try to change the icon, and our project is complete~
The source code of the project can be obtained from here .