在 iOS 上實現使用者主動觸發的 App Icon 切換
在 Emitron 專案上看到一個 App Icon 切換的功能,本文將探索並實現該功能。
Colourful Demo
新建 SwiftUI 專案,就叫它 Colourful 吧~
在 ./Colorful/Colorful 檔案加下,新增 App Icons 資料夾。借用一下 Emitron 的圖示,將這些圖示加入到 App Icons 資料夾中。每一種圖示提供四張圖片,分別是[email protected]、[email protected]、@2x、@3x。
CFBundleIcons
在 info 中 新增 Icon files (iOS 5) 欄位:
右擊Icon files (iOS 5) ,勾選 Raw Keys and Values。將列出原始 Key 名稱,而不是展示英文字地化字串。可以看到原始 Key 為 CFBundleIcons。
Newsstand
Newsstand 是 Apple 在 iOS5 推出的存放報刊雜誌類內容的 App。在 iOS9 之後,蘋果刪除了這個 App,而 CFBundleIcons 下的 UINewsstandIcon 是服務於 Newsstand 的,因此我們可以刪除
UINewsstandIcon 這個 Key。
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,表示圖示:
```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,它將處理圖示的讀取和更改,後續將繼續完善:
```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 註解的一個便捷方法:
```Swift import SwiftUI
extension View {
@ViewBuilder func if
新增 IconView.swift 檔案,畫出圖示,這裡用到了 .if:
```Swift 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,後續將展示可供更換的圖示:
```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:
```Swift 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:
```Swift import SwiftUI
struct ContentView: View { var body: some View { VStack { SettingsView() } } } ```
調整 IconManager
調整 IconManager 的 init 方法:
Swift
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。
執行程式,檢視執行情況:
繼續新增程式碼,完成 set 方法:
Swift
extension IconManager {
@MainActor func set(icon: Icon) async throws {
do {
try await UIApplication.shared.setAlternateIconName(icon.name)
currentIcon = icon
} catch {
throw error
}
}
}
調整 IconChooserView
修改程式碼,補充 Button 事件:
```Swift 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)
}
}
}
}
} ```
執行專案,嘗試更改圖示,我們的專案就完成啦~
可以從這裡獲取專案的原始碼。
- 淺析三款大規模分散式檔案系統架構設計
- 動轉靜兩大升級!一鍵轉靜成功率領先,重點模型訓練提速18%
- 文心ERNIE 3.0 Tiny新升級!端側壓縮部署“小” “快” “靈”!
- [Android禪修之路] 解讀Layer
- 35張圖,直觀理解Stable Diffusion
- Combine | (V) Combine 中的錯誤處理和 Scheduler
- Combine | (III) Combine Operator:時間操作|序列
- 「Apple Watch 應用開發系列」複雜功能(Complication)基礎
- 「Apple Watch 應用開發系列」複雜功能實踐
- 在 iOS 上實現使用者主動觸發的 App Icon 切換
- view和layer知識點整理
- 實時活動(Live Activity) - 在鎖定螢幕和靈動島上顯示應用程式的實時資料
- 「Apple Watch 應用開發系列」Dock 快照
- Layer 1新方向 Meta系公鏈Aptos、Sui 、Linera盤點
- Synapse Chain:多鏈世界的Layer0跨鏈基礎設施
- V神:什麼樣的layer 3才有意義?
- 以太坊升級將如何影響Layer2的發展?
- Vitalik:什麼樣的 Layer3 才有意義
- 從系統架構分析安全問題及應對措施
- 【雲原生】Kubernetes(K8S)與資料庫