前言
前两天开了个百度网盘的超级会员,打开百度网盘APP之后,网盘提醒我作为尊贵的SVIP用户,要不要使用能够体现自己尊贵身份的SVIP用户专属图标,这家伙,果断确定啊,然后就发现,应用图标发生了变化。
下面两张图,图 2-1是普通用户的图标,图 2-2是尊贵的SVIP专属图标,是不是瞬间感觉自己的身份就不一样了?
看到了这个功能之后,我就着手研究实现的方法。说实话,这功能,网上一搜就是一堆文章,我研究之后进行了功能实现,然后就写了这篇笔记记录下来。
设置AppIcon
每个上架App Store的应用都必须有AppIcon,所以这个没什么可说的,我的AppIcon如下图
准备应用内切换的图标
我这边准备的是每种图标准备两个,一个两倍图,一个三倍图,尺寸分别为 120*120 和 180*180,我准备的三个图标是 Mac_OS_Big_Sur_1、Mac_OS_Big_Sur_2、Mac_OS_Big_Sur_3,如下图所示:
当然,你也可以给每种图标都按照AppIcon那样配置一整套,但是要记得图片名不能重复哦。
将图标添加到项目里
在图标准备完毕后,我们将其添加到项目中,记住,放到 Assets.xcassets 里面是没有什么卵用的,我们直接自己新建文件夹,拖进去就好,如下图:
在 Info.plist 文件中添加图标
在 Info.plist 文件里的添加 Icon files(iOS 5) 这个key之后进行操作,这个key在源码里是 CFBundleIcons,基本结构如下所示:
{
"Icon files(iOS 5)": {
// 字典,我们自己添加的
"CFBundleAlternateIcons": {
// 字典,我们准备用来切换的图标,每个图标都是这样一个字典
"Mac_OS_Big_Sur_1": {
// 数组,存放我们的图标名,
// 如果我们给每个图标准备了多个图标,比如@2x、@3x,在这里面添加就好了
"CFBundleIconFiles": []
}
},
// 默认的icon,也不用管
"Primary Icon": {},
// 不用管
"Newsstand Icon": {}
}
}
我根据自己准备的图标,做了如下图的配置
相关API
以下是Apple提供的API:
extension UIApplication {
// 只读属性,用来判断是否可以修改图标
// 开发的时候可以根据这个属性,决定是否显示修改图标的入口
@available(iOS 10.3, *)
open var supportsAlternateIcons: Bool { get }
// 修改图标的主要方法,将图标名传进去就可以修改了
// 如果图标名传nil,则会将图标修改为基本图标,也就是使用AppIcon
// 注意,completionHandler 这个回调是在任意的后台队列,不是主队列,
// 要是在设置回调里修改UI之类的操作,得自己回到主队列再操作
@available(iOS 10.3, *)
open func setAlternateIconName(_ alternateIconName: String?,
completionHandler: ((Error?) -> Void)? = nil)
// 获取当前使用的图标的名称
// 如果拿到的是nil,表示使用的是AppIcon
@available(iOS 10.3, *)
open var alternateIconName: String? { get }
}
修改图标的代码
感觉就那么几个API,代码没啥可写的,随便看看API都能写出来
// 先判断能不能修改,如果能修改,直接调用系统的API进行修改
guard UIApplication.shared.supportsAlternateIcons else {
return
}
UIApplication.shared.setAlternateIconName("Mac_OS_Big_Sur_1",
completionHandler: nil)
自动获取图标列表
可以在代码中写死图标列表,也可以直接读取 Info.plist 文件中的配置信息
作为为了避免开发的时候添加了图标还得在代码中添加的麻烦,还是直接读取简单些
我在示例代码中用的是系统方法读取
要是配合 SwiftyJSON 之类的第三方框架使用,会更简单些
// 读取 CFBundleIcons 中的值,也就是 Icon files(iOS 5)这个key对应的值
guard let bundleIcons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any] else {
return
}
// 读取AppIcon对应的值
if let bundlePrimaryIcon = bundleIcons["CFBundlePrimaryIcon"] as? [String: Any] {
// 读取图标名称,设置的时候需要用到,不过AppIcon就用不到了,直接设置nil即可
let iconName = bundlePrimaryIcon["CFBundleIconName"] as? String
// 图标对应的图片列表,可以读取出来展示给用户预览
let files = bundlePrimaryIcon["CFBundleIconFiles"] as? [String] ?? []
print("AppIconName:\(iconName ?? "nil")")
print("AppIconFiles:\(files)")
}
// 读取其他用来修改的图标
if let bundleAlternateIcons = bundleIcons["CFBundleAlternateIcons"] as? [String: Any] {
// bundleAlternateIcons 中的每个键值对都是一个图标
// 因为是无序的,所以每次拿到的顺序不一定一样哦
// 要是希望顺序一样,可以根据key进行排序
bundleAlternateIcons.forEach {
// key:每个图标的名称,用来设置图标时使用
print("图标名:\($0.key)")
// value:每个图标的相关信息,包括图片列表
if let value = $0.value as? [String: Any] {
// 图标对应的图片列表,可以使用其中的一个作为展示图给用户预览
let files = value["CFBundleIconFiles"] as? [String] ?? []
print("图标\($0.key)对应的图片文件列表:\(files)")
}
}
}
隐藏修改之后的弹窗
修改之后系统会有一个弹窗告诉用户修改了图标,如果觉得不想要,可以把它隐藏掉
在修改完成后,系统会调用
UIApplication.shared.keyWindow?.rootViewController 的 func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) 方法弹出一个 UIAlertController,而且弹出来的 UIAlertController 的 title 和 message 是为 nil 的,所以我们只需要对 rootViewController 的这个方法进行进行重写,然后根据 title 和 message 是否为空决定是否调用即可,示例代码如下:
override func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil) {
if let alertVc = viewControllerToPresent as? UIAlertController {
if alertVc.title == nil && alertVc.message == nil {
return
}
}
super.present(viewControllerToPresent,
animated: flag,
completion: completion)
}
这个隐藏弹窗的方式略微有些过于暴力,而且可能会存在一些问题。
而且个人并不建议隐藏,毕竟更换图标这种事儿,还是告诉用户一下比较好。
结尾
本文是笔者通过网络查找资料等方式学习,并经过编写Demo进行验证,最终形成的一篇笔记类文章,如果文中有表述不当之处,各位可以在评论区留言交流。