該功能應(yīng)用的場(chǎng)景
1、白天/夜間模式切換,在切換App主色調(diào)同時(shí)切換App圖標(biāo)。
2、各類皮膚主題(淘寶就可換膚),附帶App圖標(biāo)一塊更換。
3、利用App圖標(biāo)表達(dá)某種特定功能,如Demo中的,提示當(dāng)前天氣。
4、圖標(biāo)促銷提示,如淘寶京東特定節(jié)日:11.11、6.18,提前更換App圖標(biāo)。
當(dāng)然該功能(API)當(dāng)前只支持iOS10.3以上的系統(tǒng),所以只能當(dāng)做一項(xiàng)附加功能來(lái)進(jìn)行使用。下面將詳細(xì)講解下如何使用代碼來(lái)實(shí)現(xiàn)此功能。
API方法
@interface UIApplication (UIAlternateApplicationIcons)`
// 如果為NO,表示當(dāng)前進(jìn)程不支持替換圖標(biāo)`
@property (readonly, nonatomic) BOOL supportsAlternateIcons NS_EXTENSION_UNAVAILABLE(``"Extensions may not have alternate icons"``) API_AVAILABLE(ios(10.3), tvos(10.2));`
// 傳入nil代表使用主圖標(biāo). 完成后的操作將會(huì)在任意的后臺(tái)隊(duì)列中異步執(zhí)行; 如果需要更改UI,請(qǐng)確保在主隊(duì)列中執(zhí)行.`
- (void)setAlternateIconName:(nullable NSString *)alternateIconName completionHandler:(nullable void (^)(NSError *_Nullable error))completionHandler NS_EXTENSION_UNAVAILABLE(``"Extensions may not have alternate icons"``) API_AVAILABLE(ios(10.3), tvos(10.2));`
// 如果alternateIconName為nil,則代表當(dāng)前使用的是主圖標(biāo).`
@property (nullable, readonly, nonatomic) NSString *alternateIconName NS_EXTENSION_UNAVAILABLE(``"Extensions may not have alternate icons"``) API_AVAILABLE(ios(10.3), tvos(10.2));`
@end
總共3個(gè)方法,簡(jiǎn)潔明了,不過(guò)但看這3個(gè)API,我們并不清楚alternateIconName是如何與app圖標(biāo)掛鉤的,所以我們需要進(jìn)一步翻看文檔!
文檔
shift+command+0打開(kāi)文檔,依次查看3個(gè)API,翻譯如下:
1.supportsAlternateIcons
(翻譯)只有系統(tǒng)允許改變你的app圖標(biāo)時(shí)該值才為YES。你需要在Info.plist文件中的CFBundleIcons這個(gè)鍵內(nèi)聲明可更換的app圖標(biāo)
2.alternateIconName
(翻譯)當(dāng)系統(tǒng)展示的是你更換后的app圖標(biāo)時(shí),該值即為圖標(biāo)名字(Info.plist中定義的圖標(biāo)名字)。如果展示的是主圖標(biāo)時(shí),這個(gè)值為nil。
3.setAlternateIconName:completionHandler:
(翻譯)alertnateIconName參數(shù):該參數(shù)為需要更換的app圖標(biāo)名字,是在你的Info.plist中的CFBundleAlertnateIcons鍵里定義的。如果你想顯示的是用CFBundlePrimaryIcon鍵所定義的主圖標(biāo)的話,就傳入nil。CFBundleAlertnateIcons與CFBundlePrimaryIcon鍵都是在CFBundleIcons里面定義的。
(翻譯)completionHandler參數(shù):該參數(shù)用來(lái)處理(更換)結(jié)果。當(dāng)系統(tǒng)嘗試更改app的圖標(biāo)后,會(huì)將結(jié)果數(shù)據(jù)通過(guò)該參數(shù)傳入并執(zhí)行(該執(zhí)行過(guò)程是在UIKit所提供的隊(duì)列執(zhí)行,并非主隊(duì)列)。該執(zhí)行過(guò)程會(huì)攜帶一個(gè)參數(shù):error。如果更換app圖標(biāo)成功,那么這個(gè)參數(shù)就是nil。如果更換過(guò)程中發(fā)生了錯(cuò)誤,那么該對(duì)象會(huì)指明錯(cuò)誤信息,并且app的圖標(biāo)保持不變。
(翻譯)使用該方法改變app圖標(biāo)為主圖標(biāo)或者可更換的圖標(biāo)。只有在supportsAlternateIcons的返回值為YES時(shí)才能更換。
(翻譯)你必須在Info.plist文件的CFBundleIcons鍵里面聲明可以更換的app圖標(biāo)(主圖標(biāo)和可更換圖標(biāo))。如果需要獲取關(guān)于可更換圖標(biāo)的配置信息,請(qǐng)查閱 Information Property List Key Reference 里面有關(guān)CFBundleIcons的描述。
- 文檔中反復(fù)提到了Info.plist文件與CFBundleIcons,這是Xcode6之前是用來(lái)配置App圖標(biāo)的老方法,后來(lái)有了更完備的Assets.scassets,配置App圖標(biāo)更簡(jiǎn)單與完善了。不過(guò)如今該方法再次被搬上臺(tái)面,在蘋果內(nèi)部一定也是歷經(jīng)多次“撕逼”后的結(jié)果,為何蘋果急于在10.3而不是11推出該API?為何蘋果不使用Assets.scassets配置可變更的App圖標(biāo)?我們不得而知,不過(guò)相信蘋果后期會(huì)對(duì)該配置方法做優(yōu)化的。
可變更App圖標(biāo)的配置方法
官方配置文檔
**該配置文檔的內(nèi)容較多,我們挑重點(diǎn)羅列下(忽略tvOS部分,下同):
Info.plist是個(gè)字典,假設(shè)為NSDictionary *infoPlist。
CFBundleIcons是Info.plist字典里的一個(gè)鍵@"CFBundleIcons"。
CFBundleIcons對(duì)應(yīng)的value是個(gè)字典。
CFBundleIcons里面能夠包含的鍵有:CFBundlePrimaryIcon、CFBundleAlternateIcons、UINewsstandIcon。
讓我們用代碼展示下這個(gè)繞口的結(jié)構(gòu):
NSDictionary *infoPlist;
infoPlist = @{
@"CFBundleIcons": @{
@"CFBundlePrimaryIcon": xxx,
@"CFBundleAlternateIcons": xxx,
@"UINewsstandIcon": xxx
}
};
這是關(guān)于CFBundleAlternateIcons的配置文檔:
其中有一句話,不仔細(xì)思考很難明
In iOS, the value of the key is a dictionary. The key for each dictionary entry is the name of the alternate icon
翻譯:該鍵對(duì)應(yīng)的值是字典,每個(gè)字典條目的鍵都是備用圖標(biāo)的名稱。
從這句話中無(wú)法很快理清CFBundleAlternateIcons下層的數(shù)據(jù)結(jié)構(gòu)。實(shí)際上這句話表達(dá)的意思是:
- 該鍵對(duì)應(yīng)的值是字典,這個(gè)字典里的每一個(gè)鍵對(duì)應(yīng)的又是一個(gè)個(gè)字典,而這些鍵都是備用圖標(biāo)的名稱。
讓我們把剩余的重點(diǎn)羅列下:
- CFBundleAlternateIcons所對(duì)應(yīng)的value是個(gè)字典(iOS中),假設(shè)為NSDictionary * alertnateIconsDic。
alertnateIconsDic的鍵,都是備用圖標(biāo)的名字,假設(shè)為@"newAppIcon"和@"newAppIcon2"。 @"newAppIcon"的value是個(gè)包含CFBundleIconFiles和UIPrerenderedIcon這兩個(gè)鍵的字典。
CFBundleIconFiles的value是字符串或者數(shù)組(數(shù)組內(nèi)容也為字符串)。字符串的內(nèi)容為各尺寸備用圖標(biāo)的名字。
UIPrerenderedIcon的value是BOOL值。這個(gè)鍵值所代表的作用在iOS7之后(含iOS7)已失效,在iOS6中可渲染app圖標(biāo)為帶高亮效果。所以這個(gè)值目前我們可以不用關(guān)心。
讓我們用代碼展示下CFBundleAlternateIcons的value的結(jié)構(gòu):
@"CFBundleAlternateIcons" : @{
@"newAppIcon" : @{
@"CFBundleIconFiles" : @[
@"newAppIcon"
],
@"UIPrerenderedIcon" : NO
},
@"newAppIcon2" : @{
@"CFBundleIconFiles" : @[
@"newAppIcon2"
],
@"UIPrerenderedIcon" : NO
}
}
實(shí)際配置文件(Info.plist)
對(duì)照著上述的配置文檔,我們實(shí)際配置完的Info.plist是這樣子的:
當(dāng)然也要拖入對(duì)應(yīng)的App圖標(biāo):
不過(guò)這里我們好像還少配置了App主圖標(biāo),也就是正常情況下我們的圖標(biāo)。按照文檔所說(shuō),我們需要在CFBundleIcons里面配置CFBundlePrimaryIcon這個(gè)主圖標(biāo)對(duì)應(yīng)的內(nèi)容,但是實(shí)際上,我們還是按照老方法,在Assets.xcassets中配置AppIcon,對(duì)應(yīng)尺寸填上對(duì)應(yīng)圖片即可。為什么這樣子就可以配置主圖標(biāo)呢?讓我們來(lái)看看某知名電商的ipa(在AppStore上下載的包)內(nèi)的Info.plist(位于Payload/XXXXXX/Info.plist):
當(dāng)然你也可以在你自己App打出的包內(nèi)進(jìn)行查看,系統(tǒng)其實(shí)是會(huì)將Assets.xcassets中配置的AppIcon轉(zhuǎn)化為Info.plist中的CFBundlePrimaryIcon。所以我們主圖標(biāo)的配置方式還是與原先一樣。
其他注意事項(xiàng):
文件擴(kuò)展名,如@2x,@3x,要么統(tǒng)一不寫,那么系統(tǒng)會(huì)自動(dòng)尋找合適的尺寸。要寫就需要把每張icon的擴(kuò)展名寫上,和上圖的格式一樣,在本系列文章的Demo中也有一個(gè)單獨(dú)的Demo示例如何添加多尺寸icon。
iPad版本如果需要有更換的圖標(biāo),需要在CFBundleIcons?ipad同樣設(shè)置一次
更換圖標(biāo)后,如何驗(yàn)證iPhone上使用了多尺寸的圖標(biāo)?
打開(kāi)DynamicAppIcon(帶尺寸)這個(gè)Demo。該Demo中,我們?cè)诟鱾€(gè)尺寸的圖標(biāo)右上角打個(gè)”標(biāo)記“,然后使用上文介紹的setAlternateIconName:completionHandler:
進(jìn)行圖標(biāo)更換。更換圖標(biāo)的同時(shí),我們?cè)僮鲆患拢?/p>
// 測(cè)試推送上是否使用了20尺寸的圖標(biāo)`
UILocalNotification *noti = [[UILocalNotification alloc] init];
noti.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];
noti.alertBody = @"我們看看推送上面的App圖標(biāo)";
[[UIApplication sharedApplication] scheduleLocalNotification:noti];
這里我們發(fā)送了一個(gè)本地通知,一會(huì)我們就能看到通知上顯示的是什么圖標(biāo)了:
看到圖標(biāo)的區(qū)別,也就說(shuō)明了我們?cè)贗nfo.plist里面設(shè)置的多尺寸圖標(biāo)生效了:
image.png
第二部分:[無(wú)彈框更換App圖標(biāo)]
什么是彈框
讓我們查看彈框的本質(zhì)
彈框與UIAlertController長(zhǎng)的倒是挺像的。讓我們來(lái)剖析下這個(gè)彈框:
可以看到彈框就是私有類_UIAlertControllerView,讓我們?cè)賹?duì)比下系統(tǒng)的UIAlertController:
所以更換App時(shí)的彈框就是UIAlertController,只不過(guò)上面的控件不太一樣罷了。(其實(shí)我們也能做到在UIAlertController上添加任意控件)
攔截彈框
既然知道了彈框是UIAlertController,那么我們自然而然想到,該彈框是由ViewController通過(guò)presentViewController:animated:completion:方法彈出。那么我們就可以通過(guò)Method swizzling hook該彈框,不讓其進(jìn)行彈出即可:
#import "UIViewController+Present.h"
#import <objc/runtime.h>
@implementation UIViewController (Present)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method presentM = class_getInstanceMethod(self.class, @selector(presentViewController:animated:completion:));
Method presentSwizzlingM = class_getInstanceMethod(self.class, @selector(dy_presentViewController:animated:completion:));
method_exchangeImplementations(presentM, presentSwizzlingM);
});
}
- (void)dy_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
if ([viewControllerToPresent isKindOfClass:[UIAlertController class]]) {
NSLog(@"title : %@",((UIAlertController *)viewControllerToPresent).title);
NSLog(@"message : %@",((UIAlertController *)viewControllerToPresent).message);
UIAlertController *alertController = (UIAlertController *)viewControllerToPresent;
if (alertController.title == nil && alertController.message == nil) {
return;
} else {
[self dy_presentViewController:viewControllerToPresent animated:flag completion:completion];
return;
}
}
[self dy_presentViewController:viewControllerToPresent animated:flag completion:completion];
}
這段代碼交換了UIViewController的presentViewController:animated:completion:方法。通過(guò)打印UIAlertController的特征,我們可以發(fā)現(xiàn),更換App圖標(biāo)時(shí)的彈框是沒(méi)有title與message的,但是我們一般使用的UIAlertController都是帶title、message的,畢竟不會(huì)彈個(gè)空白的框給用戶玩。
所以該方法中通過(guò)判斷title與message來(lái)捕捉更換App圖標(biāo)時(shí)的彈框,并直接return即可。
總結(jié)
其實(shí)關(guān)于界面上的東西,利用動(dòng)態(tài)特性沒(méi)有什么是不能做的,蘋果既然公開(kāi)了動(dòng)態(tài)API,我們就可以通過(guò)動(dòng)態(tài)方法去了解甚至改造我們想要的東西,如系統(tǒng)控件如何實(shí)現(xiàn)等。蘋果的”規(guī)范“在應(yīng)用層面其實(shí)是無(wú)法阻擋開(kāi)發(fā)者步伐的,當(dāng)然動(dòng)態(tài)特性也不能夠?yàn)E用(如私有方法),畢竟審核人員才是爸爸!
盡管目前實(shí)現(xiàn)了在用戶無(wú)感的情況下替換App圖標(biāo),但是可替換的圖標(biāo)還是必須預(yù)先放入工程中,并且要在Info.plist內(nèi)指定。這很大程度上限制了更換圖標(biāo)的動(dòng)態(tài)性:比如我們某天想要推出一款新主題以及對(duì)應(yīng)的App圖標(biāo),但是新的App圖標(biāo)并沒(méi)有預(yù)先放入工程的main bundle中,也沒(méi)有在Info.plist中進(jìn)行指定,所以我們?cè)诓簧霞苄掳姹镜那闆r下,無(wú)法推出該新App圖標(biāo)
最后附上核心代碼:
- (void)setAppIconWithName:(NSString *)iconName {
if (![[UIApplication sharedApplication] supportsAlternateIcons]) {
return;
}
if ([iconName isEqualToString:@""]) {
iconName = nil;
}
[[UIApplication sharedApplication] setAlternateIconName:iconName completionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"更換發(fā)生錯(cuò)誤了 : %@",error);
}
}];
}