在 SwiftUI 中,可以使用 UIApplication 提供的接口来更换应用图标。
需要注意的是,这个方法适用于iOS 15.0+,可能不适应于之前的iOS版本。
1、上传应用备选图标
在Xcode中找到Assets资源文件夹,点击底部新增按钮,找到iOS – iOS App icon。
点击后,会自动创建一个图标占位符。将图标重新命名,并上传备选图标。
默认的主图标通常命名为 AppIcon,替代图标可以添加在 AppIcon 下,命名为希望使用的图标名称(如 AppIcon0、AppIcon1 等)。
建议将上传的图标放在一个单独的文件夹中,避免与其他图片混淆,因为图标和其他图片不同,图标不可以用Image直接访问,需要UIImage访问。
我在这里创建了AppIcon0、AppIcon1…AppIcon4,共五个备选图标。
2、配置Build Settings
在Xcode文件中,点击左上角的项目图标,找到TARGETS – Build Settings,在输入框中搜索“icon”。
找到“Alternate App Icon Sets字段”,在可编辑的一栏中双击打开,并输入可更换的应用图标。
输入的名称和上传的应用备选图标名称一致。
3、SwiftUI更换图标
在SwiftUI文件中,首先创建一个方法:
// 更换图标方法
func setAlternateIconNameFunc(name: String) {
UIApplication.shared.setAlternateIconName(name == "AppIcon" ? nil : name)
}
这个setAlternateIconName方法可以用于更换应用图标。
var appIcon: [Int] {
return Array(isInAppPurchase ? 0..<5 : 0..<2)
}
ForEach(appIcon, id: \.self) { index in
Button(action: {
setAlternateIconNameFunc(name: "AppIcon\(index)")
}, label: {
Rectangle()
.foregroundColor(.white)
.frame(width: isPadScreen ? 180 : 100,height: isPadScreen ? 180 : 100)
.cornerRadius(10)
.clipped()
.overlay {
Image(uiImage: UIImage(named: "AppIcon\(index)") ?? UIImage())
.resizable()
.scaledToFill()
.frame(width: isPadScreen ? 175 : 95,height: isPadScreen ? 175 : 95)
.cornerRadius(10)
.clipped()
}
})
}
在视图中使用ForEach循环遍历,我这里设置了一个appIcon数组,如果是内购用户,显示五个图标,如果普通用户,则显示两个图标,可以根据自身需求设定范围。
ForEach遍历这个appIcon数组,在ForEach中放入一个Button按钮,内容是一个简单的背景和图标的嵌套,实际需要了解的内容是,因为图标和普通照片不一样,不能直接使用Image(:_)获取,而是需要使用UIImage获取图标。
Image(uiImage: UIImage(named: "AppIcon\(index)") ?? UIImage())
这里通过ForEach获取index,并显示图标到屏幕上。
当点击按钮时,会执行setAlternateIconNameFunc方法:
setAlternateIconNameFunc(name: "AppIcon\(index)")
在这个方法中,如果传入的name是AppIcon,将setAlternateIconName设置为nil,表示使用应用的原始图标。如果传入其他name,则使用对应的名称。
// 更换图标方法
func setAlternateIconNameFunc(name: String) {
UIApplication.shared.setAlternateIconName(name == "AppIcon" ? nil : name)
}
这里传入的name,就与我们前面提到的 Build Settings 中配置的内容一致。
如果Build Settings配置错了,或者name传入错了,图标是无法配置成功的。
4、实现效果
在实现效果中,可以看到当点击图标后,会执行setAlternateIconNameFunc方法,系统会弹出一个已更改图标的弹窗,点击完成后,图标修改完成。
5、当前图标
如果想要获取当前的图标,可以创建一个变量,通过alternateIconName返回当前图标的名称。
var AlternateIconName: String {
UIApplication.shared.alternateIconName ?? "AppIcon"
}
在图标列表中,我给底部的矩形新增了一个边框,如果AlternateIconName与当前的AppIcon相等,那么显示这个边框,否则不显示。
Button(action: {
setAlternateIconNameFunc(name: "AppIcon\(index)")
}, label: {
Rectangle()
.strokeBorder(AlternateIconName == "AppIcon\(index)" ? Color(hex:"FF4B00") : .clear, lineWidth: 5)
.foregroundColor(.white)
.frame(width: isPadScreen ? 180 : 100,height: isPadScreen ? 180 : 100)
.cornerRadius(10)
.clipped()
.overlay {
Image(uiImage: UIImage(named: "AppIcon\(index)") ?? UIImage())
.resizable()
.scaledToFill()
.frame(width: isPadScreen ? 175 : 95,height: isPadScreen ? 175 : 95)
.cornerRadius(10)
.clipped()
}
})
运行代码后,在模拟器中选择灰色的图标,可以看到灰色的图标外层带有黄色的边框,表示选中。
更多细节,可以参考Apple提供的代码示例。
总结
通过以上方法就可以使用应用图标的更换,还需要注意的一点,那就是Xcode项目应用名称的名称为AppIcon,因此如果通过ForEach遍历,实际上是从0到N的遍历,没有涉及到原始图标的遍历。
因此,这里可以在ForEach前面单独加一个Button按钮,显示原始图标:
Button(action: {
setAlternateIconNameFunc(name: "AppIcon")
}, label: {
Rectangle()
.strokeBorder(AlternateIconName == "AppIcon" ? Color(hex:"FF4B00") : .clear, lineWidth: 5)
.foregroundColor(.white)
.frame(width: isPadScreen ? 180 : 100,height: isPadScreen ? 180 : 100)
.cornerRadius(10)
.clipped()
.overlay {
Image(uiImage: UIImage(named: "AppIcon") ?? UIImage())
.resizable()
.scaledToFill()
.frame(width: isPadScreen ? 175 : 95,height: isPadScreen ? 175 : 95)
.cornerRadius(10)
.clipped()
}
})
参考文章
1、Configuring Your App to Use Alternate App Icons:https://developer.apple.com/documentation/xcode/configuring_your_app_to_use_alternate_app_icons
2、setAlternateIconName(_:completionHandler:):https://developer.apple.com/documentation/uikit/uiapplication/setalternateiconname(_:completionhandler:)
3、Alternate App Icon Configuration in Xcode:https://www.avanderlee.com/swift/alternate-app-icon-configuration-in-xcode/
4、新版iOS应用更换图标开发教程,用户自定义图标开发:
https://blog.zhheo.com/p/9b28e469.html
完整代码
import SwiftUI
struct AppIconView: View {
@AppStorage("20240523") var isInAppPurchase = false // 内购完成后,设置为true
@Environment(\.layoutDirection) var layoutDirection // 获取当前语言的文字方向
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
// 鸣谢页面
let columns = [
GridItem(.adaptive(minimum: 80, maximum: 160)), // 自动根据屏幕宽度生成尽可能多的单元格,宽度最小为 80 点
GridItem(.adaptive(minimum: 80, maximum: 160)),
GridItem(.adaptive(minimum: 80, maximum: 160))
]
let columnsIpad = [
GridItem(.adaptive(minimum: 130, maximum: 200)), // 自动根据屏幕宽度生成尽可能多的单元格,宽度最小为 80 点
GridItem(.adaptive(minimum: 130, maximum: 200)),
GridItem(.adaptive(minimum: 130, maximum: 200))
]
var appIcon: [Int] {
// return Array(isInAppPurchase ? 0..<5 : 0..<2)
Array(0..<5)
}
var AlternateIconName: String {
UIApplication.shared.alternateIconName ?? "AppIcon"
}
// 更换图标方法
func setAlternateIconNameFunc(name: String) {
UIApplication.shared.setAlternateIconName(name == "AppIcon" ? nil : name)
}
var body: some View {
NavigationStack {
GeometryReader { geometry in
// 通过 `geometry` 获取布局信息
let width = geometry.size.width * 0.85
ZStack {
// 背景
Color(hex: colorScheme == .light ? "f0f0f0" : "0E0E0E")
.ignoresSafeArea()
ScrollView(showsIndicators: false ) {
LazyVGrid(columns: isPadScreen ? columnsIpad : columns,spacing: 20) {
Button(action: {
setAlternateIconNameFunc(name: "AppIcon")
}, label: {
Rectangle()
.strokeBorder(AlternateIconName == "AppIcon" ? Color(hex:"FF4B00") : .clear, lineWidth: 5)
.foregroundColor(.white)
.frame(width: isPadScreen ? 180 : 100,height: isPadScreen ? 180 : 100)
.cornerRadius(10)
.clipped()
.overlay {
Image(uiImage: UIImage(named: "AppIcon") ?? UIImage())
.resizable()
.scaledToFill()
.frame(width: isPadScreen ? 175 : 95,height: isPadScreen ? 175 : 95)
.cornerRadius(10)
.clipped()
}
})
ForEach(appIcon, id: \.self) { index in
Button(action: {
setAlternateIconNameFunc(name: "AppIcon\(index)")
}, label: {
Rectangle()
.strokeBorder(AlternateIconName == "AppIcon\(index)" ? Color(hex:"FF4B00") : .clear, lineWidth: 5)
.foregroundColor(.white)
.frame(width: isPadScreen ? 180 : 100,height: isPadScreen ? 180 : 100)
.cornerRadius(10)
.clipped()
.overlay {
Image(uiImage: UIImage(named: "AppIcon\(index)") ?? UIImage())
.resizable()
.scaledToFill()
.frame(width: isPadScreen ? 175 : 95,height: isPadScreen ? 175 : 95)
.cornerRadius(10)
.clipped()
}
})
}
}
.frame(width: width)
.frame(maxWidth: .infinity,maxHeight: .infinity)
.navigationTitle("App Icon")
.navigationBarTitleDisplayMode(.inline)
.padding(.vertical,20)
}
}
}
}
}
}