Implementing App Icon Switching Actively Triggered by Users on iOS

See an App Icon switching function on the Emitron project , this article will explore and implement this function.

image

Colourful Demo

Create a new SwiftUI project, let’s call it Colorful~

image

image

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.

image

image

CFBundleIcons

Add Icon files (iOS 5) field in info:

image

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.

image

image

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 中。

image

  • 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 相关内容。

image

image

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

image

image

CFBundleAlternateIcons

此 Key 标识 App 的备用图标,需要我们手动添加。

image

UINewsstandBindingType、UINewsstandBindingEdge 如上文我们并不需要,手动进行删除。而光泽效果 UIPrerenderedIcon,需要我们手动添加。而 Emitron 的效果是多张 App Icon,因此,我们需要对 CFBundleAlternateIcons 的结构进行调整。根据 Apple 文档,在 iOS 中,CFBundleAlternateIcons 的值是一个字典。每个字典条目的键是备用图标的名称。根据我们的备用图标 black-white、white-black、multi-black、black-multi,我们调整结构如下:

image

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)
        }
    }
}
复制代码

image

新增 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:

image

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~

iShot_2022-09-25_20.59.31.gif

The source code of the project can be obtained from here .

Guess you like

Origin juejin.im/post/7147303035923300365