iOS开发攻城狮的集散地iOS 笔记

iOS 动态更换App图标

2018-06-28  本文已影响22人  Dayon

该功能应用的场景

当然该功能(API)当前只支持iOS10.3以上的系统,所以只能当做一项附加功能来进行使用。下面将详细讲解下如何使用代码来实现此功能。

API与文档

API方法

@interface UIApplication (UIAlternateApplicationIcons)`

// 如果为NO,表示当前进程不支持替换图标`

@property (readonly, nonatomic) BOOL supportsAlternateIcons NS_EXTENSION_UNAVAILABLE(``"Extensions may not have alternate icons"``) API_AVAILABLE(ios(10.3), tvos(10.2));`

// 传入nil代表使用主图标. 完成后的操作将会在任意的后台队列中异步执行; 如果需要更改UI,请确保在主队列中执行.`

- (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,则代表当前使用的是主图标.`

@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个方法,简洁明了,不过但看这3个API,我们并不清楚alternateIconName是如何与app图标挂钩的,所以我们需要进一步翻看文档!

文档

shift+command+0打开文档,依次查看3个API,翻译如下:

1.supportsAlternateIcons

supportsAlternateIcons Document

(翻译)只有系统允许改变你的app图标时该值才为YES。你需要在Info.plist文件中的CFBundleIcons这个键内声明可更换的app图标

2.alternateIconName

alternateIconName

(翻译)当系统展示的是你更换后的app图标时,该值即为图标名字(Info.plist中定义的图标名字)。如果展示的是主图标时,这个值为nil。

3.setAlternateIconName:completionHandler:

setAlternateIconName Document

(翻译)alertnateIconName参数:该参数为需要更换的app图标名字,是在你的Info.plist中的CFBundleAlertnateIcons键里定义的。如果你想显示的是用CFBundlePrimaryIcon键所定义的主图标的话,就传入nil。CFBundleAlertnateIcons与CFBundlePrimaryIcon键都是在CFBundleIcons里面定义的。

(翻译)completionHandler参数:该参数用来处理(更换)结果。当系统尝试更改app的图标后,会将结果数据通过该参数传入并执行(该执行过程是在UIKit所提供的队列执行,并非主队列)。该执行过程会携带一个参数:error。如果更换app图标成功,那么这个参数就是nil。如果更换过程中发生了错误,那么该对象会指明错误信息,并且app的图标保持不变。

(翻译)使用该方法改变app图标为主图标或者可更换的图标。只有在supportsAlternateIcons的返回值为YES时才能更换。

(翻译)你必须在Info.plist文件的CFBundleIcons键里面声明可以更换的app图标(主图标和可更换图标)。如果需要获取关于可更换图标的配置信息,请查阅 Information Property List Key Reference 里面有关CFBundleIcons的描述。

可变更App图标的配置方法

官方配置文档

官方配置文档

**该配置文档的内容较多,我们挑重点罗列下(忽略tvOS部分,下同):

让我们用代码展示下这个绕口的结构:

    NSDictionary *infoPlist;
    infoPlist = @{
                  @"CFBundleIcons": @{
                          @"CFBundlePrimaryIcon": xxx,
                          @"CFBundleAlternateIcons": xxx,
                          @"UINewsstandIcon": xxx
                          }
                  };
image.png 2.png

这是关于CFBundleAlternateIcons的配置文档:

其中有一句话,不仔细思考很难明

从这句话中无法很快理清CFBundleAlternateIcons下层的数据结构。实际上这句话表达的意思是:

让我们把剩余的重点罗列下:

alertnateIconsDic的键,都是备用图标的名字,假设为@"newAppIcon"和@"newAppIcon2"。 @"newAppIcon"的value是个包含CFBundleIconFiles和UIPrerenderedIcon这两个键的字典。

让我们用代码展示下CFBundleAlternateIcons的value的结构:

    @"CFBundleAlternateIcons" : @{
                                  @"newAppIcon" : @{
                                          @"CFBundleIconFiles" : @[
                                                  @"newAppIcon"
                                                  ],
                                          @"UIPrerenderedIcon" : NO
                                          },
                                  @"newAppIcon2" : @{
                                          @"CFBundleIconFiles" : @[
                                                  @"newAppIcon2"
                                                  ],
                                          @"UIPrerenderedIcon" : NO
                                          }
                                  }

实际配置文件(Info.plist)

对照着上述的配置文档,我们实际配置完的Info.plist是这样子的:

1242701-ced50e192daf6cf5.png

当然也要拖入对应的App图标:

1242701-cf0f7def080054f6.png

不过这里我们好像还少配置了App主图标,也就是正常情况下我们的图标。按照文档所说,我们需要在CFBundleIcons里面配置CFBundlePrimaryIcon这个主图标对应的内容,但是实际上,我们还是按照老方法,在Assets.xcassets中配置AppIcon,对应尺寸填上对应图片即可。为什么这样子就可以配置主图标呢?让我们来看看某知名电商的ipa(在AppStore上下载的包)内的Info.plist(位于Payload/XXXXXX/Info.plist):

1242701-ec188d5725247010.png

当然你也可以在你自己App打出的包内进行查看,系统其实是会将Assets.xcassets中配置的AppIcon转化为Info.plist中的CFBundlePrimaryIcon。所以我们主图标的配置方式还是与原先一样。

其他注意事项:

文件扩展名,如@2x,@3x,要么统一不写,那么系统会自动寻找合适的尺寸。要写就需要把每张icon的扩展名写上,和上图的格式一样,在本系列文章的Demo中也有一个单独的Demo示例如何添加多尺寸icon。

iPad版本如果需要有更换的图标,需要在CFBundleIcons?ipad同样设置一次

更换图标后,如何验证iPhone上使用了多尺寸的图标?

1242701-42c32db9f400c003.png

打开DynamicAppIcon(带尺寸)这个Demo。该Demo中,我们在各个尺寸的图标右上角打个”标记“,然后使用上文介绍的setAlternateIconName:completionHandler:

进行图标更换。更换图标的同时,我们再做一件事:

// 测试推送上是否使用了20尺寸的图标`

UILocalNotification *noti = [[UILocalNotification alloc] init];

noti.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];

noti.alertBody = @"我们看看推送上面的App图标";

[[UIApplication sharedApplication] scheduleLocalNotification:noti];

这里我们发送了一个本地通知,一会我们就能看到通知上显示的是什么图标了:

1242701-9277f4ed3d250bbb.png

看到图标的区别,也就说明了我们在Info.plist里面设置的多尺寸图标生效了:

1.png

image.png

第二部分:[无弹框更换App图标]

什么是弹框

让我们查看弹框的本质

弹框与UIAlertController长的倒是挺像的。让我们来剖析下这个弹框:

1242701-7312a5e6f99df28f.png

可以看到弹框就是私有类_UIAlertControllerView,让我们再对比下系统的UIAlertController:

3.png

所以更换App时的弹框就是UIAlertController,只不过上面的控件不太一样罢了。(其实我们也能做到在UIAlertController上添加任意控件)

拦截弹框

既然知道了弹框是UIAlertController,那么我们自然而然想到,该弹框是由ViewController通过presentViewController:animated:completion:方法弹出。那么我们就可以通过Method swizzling hook该弹框,不让其进行弹出即可:

#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:方法。通过打印UIAlertController的特征,我们可以发现,更换App图标时的弹框是没有title与message的,但是我们一般使用的UIAlertController都是带title、message的,毕竟不会弹个空白的框给用户玩。

所以该方法中通过判断title与message来捕捉更换App图标时的弹框,并直接return即可。

总结

最后附上核心代码:

- (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(@"更换发生错误了 : %@",error);
        }
    }];
}
上一篇下一篇

猜你喜欢

热点阅读