Implementación del cambio de icono de aplicación activado activamente por los usuarios en iOS

Vea una función de cambio de icono de aplicación en el proyecto Emitron , este artículo explorará e implementará esta función.

imagen

Demostración colorida

Cree un nuevo proyecto SwiftUI, llamémoslo Colorful~

imagen

imagen

Agregue la carpeta Iconos de la aplicación en el archivo ./Colorful/Colorful. Tome prestados los íconos de Emitron y agréguelos a la carpeta de íconos de la aplicación. Cada icono proporciona cuatro imágenes, a saber, -ipad@2x, -ipadpro@2x, @2x, @3x.

imagen

imagen

CFBundleIcons

Agregue el campo de archivos de iconos (iOS 5) en la información:

imagen

Haga clic con el botón derecho en Archivos de iconos (iOS 5) y marque Claves y valores sin procesar. El nombre de la clave original aparecerá en la lista en lugar de mostrar la cadena localizada en inglés. Puede ver que la clave original es CFBundleIcons.

imagen

imagen

Puesto de periódicos

Kiosco es una aplicación lanzada por Apple en iOS5 para almacenar contenido de periódicos y revistas. Después de iOS9, Apple eliminó esta aplicación y el UINewsstandIcon en CFBundleIcons es para Newsstand, por lo que podemos eliminarlo.

La clave de UINewsstandIcon.

CFBundlePrimaryIcon

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

imagen

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

imagen

imagen

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

imagen

imagen

CFBundleAlternateIcons

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

imagen

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

Aquí, el nombre del ícono actual se obtiene primero. Dado que nuestro ícono principal no tiene nombre, currentIconName está vacío. icons es una matriz de íconos primarios y alternativos. currentIcon es el icono principal actual.

Ejecute el programa para ver cómo funciona:

image

Continúe agregando código para completar el método set:

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

Ajustar IconChooserView

Modifique el código para agregar el evento 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)
                }
            }
        }
    }
}
复制代码

Ejecute el proyecto, intente cambiar el icono y nuestro proyecto estará completo~

iShot_2022-09-25_20.59.31.gif

El código fuente del proyecto se puede obtener desde aquí .

Supongo que te gusta

Origin juejin.im/post/7147303035923300365
Recomendado
Clasificación