APP动态换肤方案详解
换肤背景
客户体验是衡量一款APP质量的重要考核点,而换肤则是提升客户体验的重要一环。换肤包括但不限于APP主动更换主题(比方根据春节、圣诞、元旦等节假日更换节日主题)、局部页面换肤(白天夜间模式切换、阅读页面字体颜色的调整)、APP客户自己设置皮肤的编辑等等。这些在现如今的主流APP上都能找到身影,甚至iOS系统在iOS13之后就已经提供了暗黑模式以用于换肤的实现。实现换肤方案需要考虑的要点主要包含以下几方面:
换肤元素
换肤最终表现在视觉层面的体现一般是:图片、字体、颜色的改变,当然也可能包括布局排版、动画特效的变动,但后两者涉及的需求场景较少,所以不在本文的探讨范围内。
皮肤资源管理
皮肤包内会包含图片、颜色色值、字体包等各种皮肤资源,它们是构成换肤的最底层要素,能够便捷有效地管理皮肤资源是实现换肤方案中的重要一环。皮肤资源管理具体包括如何在尽可能减少开发量的情况下快速更换皮肤资源打包生成不同样式的渠道包,APP内存在多套内置皮肤资源又应该怎么管理,是否在APP不更新发版的情况下在线升级皮肤等等。
换肤框架的易用性
开发者引入换肤框架能否会添加额外大量的工作量,换肤框架提供的API接口复不复杂,换肤框架与原有项目的耦合性高不高……这些都是设计一款换肤方案需要考虑的要点。
换肤类型
根据换肤方式的不同,主要分为两类。
本地换肤
APP所能支持的换肤资源在APP打包初始化时就已确定,安装之后不能再动态下载升级,假如升级必需重新打包送审,其中最具代表性的便是iOS13之后系统支持的暗黑与明亮模式。当然也可以在打包时提前将多套皮肤资源内置到APP资源包中,后面再由接口控制皮肤切换。本地换肤的缺点是换肤不够灵活,并且包的体积过大,还依赖苹果审核。
远程换肤
换肤资源由接口下发,下发后可选择为增量升级或者者是等一律图片皮肤资源下载完成后再全局换肤。至于皮肤资源的下载时机则可以根据业务的实际需要设计为客户主动点击触发,或者者读取后端配置自动升级。
现有换肤方案分析
分析市面上已有换肤方案的实现,大概分为以下几类。
1、分类(Category)
在分类中添加扩展属性,并用扩展属性来设置皮肤样式,比方给UILabel添加扩展属性skin_textColor
,该属性可以根据当前皮肤主题读取对应的文本颜色用以显示,使用时弃用系统原有的textColor
,改用skin_textColor
来设置样式。这种方案的不足点是需要对所有的UI控件以及每一个控件对应的UI属性都进行分类重写,这样的工作量是巨大的,而且换肤方案没法覆盖那些自己设置的UI组件,由于你无法预知自己设置UI组件的样式是怎么的。另一方面开发者引入换肤框架后还得对所在项目代码进行大量修改,毕竟你得将系统原有的样式属性设置改为扩展属性后换肤才会生效。
2、基于UIView的分类实现
该方案是基于UI组件都是继承自UIView
的前提下实现的,在UIView
的分类中添加扩展属性skinMap
,用于记录了当前组件所要执行的所有换肤方法信息,存储时key为当前调用方法名,value为皮肤资源名称;同时在分类中统一重写所有UI组件的样式设置方法。当发生换肤事件后由换肤管理中心发出换肤通知,所有接收到通知的UI组件对象都会查找执行自身已存储的换肤方法,自动触发样式刷新。
@interface UIView (Skin)@property (nonatomic, copy) NSDictionary *skinMap;@end@implementation UIView (Skin)static void *kUIViewSkinMap;- (void)setSkinMap:(NSDictionary *)skinMap { objc_setAssociatedObject(self, &kUIViewSkinMap, skinMap, OBJC_ASSOCIATION_COPY_NONATOMIC); // 判断能否已注册监听换肤通知 if (!_registerSkinNotic) { // 注册监听换肤通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(skinChanged:) name:kSkinDidChangeNotification object:nil]; } // 触发样式设置 [self skinChanged:nil];}- (NSDictionary *)skinMap { return objc_getAssociatedObject(self, &kUIViewSkinMap);}- (void)skinChanged:(NSDictionary *)notic { NSDictionary *skinMap = self.skinMap; if ([self isKindOfClass:[UILabel class]]) { UILabel *obj = (UILabel *)self; if (skinMap[NSStringFromSelector(@selector(setTextColor:))]) { obj.textColor = [SkinManager colorWithKey:skinMap[NSStringFromSelector(@selector(setTextColor:))]]; } if (skinMap[NSStringFromSelector(@selector(setBackgroundColor:))]) { obj.backgroundColor = [SkinManager colorWithKey:skinMap[NSStringFromSelector(@selector(setBackgroundColor:))]]; } //...依次重写解决UILabel的其余所有属性 } if ([self isKindOfClass:[UIButton class]]) { UIButton *obj = (UIButton *)self; if (skinMap[NSStringFromSelector((@selector(setTitleColor:forState:))]) { [obj setTitleColor:skinMap[NSStringFromSelector(@selector(setTitleColor:forState:))] forState:UIControlStateNormal]; } //...依次重写解决UIButton的其余所有属性 } //if([self isKindOfClass:[UIImageView class]]){} //...依次重写解决其余所有UI组件}@end //换肤设置示例[UILabel new].skinMap = @{NSStringFromSelector(@selector(setTextColor:)):@"文本颜色", NSStringFromSelector(@selector(setBackgroundColor:)):@"背景颜色"};
这种方案和方案1相似同样对UI控件的样式设置方法进行了重写,并且它的重写是统一封装在了UIView分类方法的内部,尽管说理论上能够做到对系统已有UI控件的所有属性都重写覆盖,但其扩展性还是比较差的。当系统框架新添加了样式属性,或者者是由开发者开发的其余第三方UI控件的样式属性,想要进行换肤设置那该方案也就无能为力了。
3、转发并缓存属性设置方法
新建NSObject的分类并实现一个相似performSelector:withObject:
的调用方法,在该调用方法内转发本来的样式设置方法,同时注册监听换肤通知以及保存当前样式设置方法的缓存。当接收到换肤通知后,当前UI组件对象同样会遍历执行自身已存储的所有方法缓存,自动触发样式刷新。
@implementation NSObject (Skin)- (void)CJSkinInvokeMethodForSelector:(SEL)aSelector withArguments:(NSArray *)params { // 1、 判断能否已注册监听换肤通知 if (!_registerSkinNotic) { // 注册监听换肤通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(skinChanged:) name:kSkinDidChangeNotification object:nil]; } // 2、存储当前方法缓存 [self cacheParams:params forSelector:aSelector]; // 3、 转发本来的属性设置方法 [self performSelector:aSelector params:params];}- (void)performSelector:(SEL)aSelector withParams:(NSArray *)params { // 使用NSInvocation进行方法转发 NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:aSelector]]; [inv setTarget:self]; [inv setSelector:aSelector]; [inv setArgument:¶ms atIndex:index]; [inv invoke];}- (void)skinChanged:(NSDictionary *)notic { for (NSString *selName in [self.cachedMethods allKeys]){ NSArray *params = self.cachedMethods[selName]; [self performSelector:NSSelectorFromString(selName) params:params]; }}@end //换肤设置示例[[UILabel new] CJSkinInvokeMethodForSelector:@selector(setBackgroundColor:) withArguments:@[[SkinManager colorWithKey:key]]];
这种方案应该是所有换肤方案中设计思路最好的一种,它很好的解耦了组件本来的样式设置方法与换肤框架的耦合关系,理论上该方案无需修改就能够满足所有UI组件的换肤设置。然而此种换肤方案在设计上还是有少量需要注意的地方:一是换肤资源的管理应该和换肤框架尽可能地解耦,由于在实际开发中,皮肤资源是免不了必需要由开发者来管理配置的,而至于换肤框架的内部实现则不必关心。二是换肤框架要能够根据当前皮肤主题自动映射显示对应的皮肤样式,而且这一步的操作应该封装在框架内部,而不是由开发者另外去判断设置。三是皮肤资源要能够支持在线增删改,这也对应了前面所说的换肤类型中的远程换肤。但是市面上依据此种思路设计的换肤方案并没有很好的处理上面所说的三个问题。
综上可以看出,以上的换肤方案都多多少少会有存在不足。也正是基于此,我重新设计实现了一套完整的动态换肤方案,下面我们就来分析一下。
动态换肤方案
皮肤资源读取
换肤架构
从下往上,最底层是皮肤资源管理模块,模块内可以内置皮肤包1、皮肤包2、皮肤包3...
多个皮肤资源包,每一个皮肤包内则包含颜色、图片、字体三类皮肤资源的配置信息。另外也可以在后期通过接口远程升级皮肤包,需要升级的皮肤包需要按照下图所示固定格式的方式来生成压缩包。
换肤资源管理
换肤资源使用 CJSkin.plist 文件(文件名固定)来配置管理换肤信息。如下图所示:当前项目的CJSkin.plist文件内记录了default、skin1、skin2三个皮肤包,每个皮肤包内固定包含Color
、Image
、Font
(颜色、图片、字体)三类皮肤元素的信息。
不同皮肤包 Color 字典中的key相同值不同:比方default皮肤包中 导航背景色
值为0x996666,skin2皮肤包中 导航背景色
的值为0x454545。
Image 的说明同理,比方default和skin2皮肤包中都有 顶部图片
,但分别指向了不同的url;另外不同皮肤包的图片还可以放到各自的default.bundle、skin1.bundle文件夹内,同时在CJSkin.plist中公告图片别名,比方skin1.bundle中包含图片top.png,它在CJSkin.plist的配置为“ 顶部图片 : top.png
”。
Font 的配置说明也是一样,不同皮肤包的key相同,值为包含Name、Size两个固定key的字典,Name为空则使用系统默认字体,Size表示了字号大小。
CJSkin.plist
新建皮肤资源管理工具类 CJSkinTool,通过它可以便捷获取当前皮肤包下的资源信息。
//从当前皮肤包,快速获取颜色FOUNDATION_EXPORT UIColor* SkinColor(NSString *key);//从当前皮肤包,快速获取图片(只能获取本地图片)FOUNDATION_EXPORT UIImage* SkinImage(NSString *key);//从当前皮肤包,快速获取字体FOUNDATION_EXPORT UIFont* SkinFont(NSString *key);/** 快速获取皮肤资源,颜色转换工具类实例 */FOUNDATION_EXPORT CJSkinTool* SkinColorTool(NSString *key);/** 快速获取皮肤资源,图片转换工具类实例 */FOUNDATION_EXPORT CJSkinTool* SkinImageTool(NSString *key);/** 快速获取皮肤资源,字体转换工具类实例 */FOUNDATION_EXPORT CJSkinTool* SkinFontTool(NSString *key);@interface CJSkinTool : NSObject/**皮肤资源key */@property (nonatomic, copy, readonly) NSString *key;/**皮肤资源类型 0颜色,1图片,2字体 */@property (nonatomic, assign, readonly) CJSkinValueType valueType;/** 从当前皮肤包初始化皮肤资源的实例方法 @param key 皮肤值的key @param type 皮肤值的类型 @return CJSkinTool */+ (CJSkinTool *)skinToolWithKey:(NSString *)key type:(CJSkinValueType)type;/** 获取皮肤资源 @return UIColor、UIImage或者者UIFont */- (id)skinValue;@end
静态换肤
有了前面皮肤资源管理工具类 CJSkinTool的支持,到此已经完全可以支持静态换肤的实现。这里说的静态换肤是指UI组件能够根据当前皮肤主题显示对应的样式,但当APP发生皮肤切换事件后,UI组件和所在页面不会自动刷新样式,只能重新设置或者者页面重载才可升级换肤。
UIButton *button = [UIButton new];button.backgroundColor = SkinColor(@"背景色");[button setImage:SkinImage(@"按钮") forState:UIControlStateNormal];button.titleLabel.font = SkinFont(@"标题");
动态换肤
动态换肤基于前面讲到的“现有换肤方案分析”—— 转发并缓存属性设置方法的原理实现,总体的换肤流程见下图。
换肤流程
动态换肤框架在基于方法转发的基础上,针对前面提到的问题要点做了改进:
动态换肤方法内封装解决了根据当前皮肤主题自动读取对应皮肤资源的逻辑,还有便是对于动态转发方法的参数进行了链式分析,由于传入的参数本身可能就是调用函数或者者是NSArray类型的参数集合。例如将前面静态换肤中UIButton的样式设置改为动态换肤则是这样:
UIButton *button = [UIButton new]; [button CJSkinInvokeMethodForSelector:@selector(setBackgroundColor:) withArguments:@[SkinColorTool(@"背景色")]]; [button CJSkinInvokeMethodForSelector:@selector(setImage:forState:) withArguments:@[SkinImageTool(@"按钮"),@(UIControlStateNormal)]]; [button.titleLabel CJSkinInvokeMethodForSelector:@selector(setFont:) withArguments:@[SkinFontTool(@"标题")]];
可能你已经注意到了,动态换肤设置颜色的参数
SkinColorTool(@"背景色")
其实是一个CJSkinTool
对象,这是由于CJSkinInvokeMethodForSelector:withArguments:
动态转发方法内对CJSkinTool
类型的参数做了特殊解决,首先根据CJSkinTool
的key读取生成当前皮肤主题下对应的UIColor
色值,而后才继续转发样式设置方法,图片字体的解决逻辑同理。支持在线图片样式的设置,换肤框架当判断到需要设置的是在线图片时会自动下载,并在下载完成后才进行样式升级。假设有一张key为 按钮 的图片,
皮肤包1
中公告的是本地图片,皮肤包2
中的公告是网络图片,那么切换皮肤1会读取本地图片即时刷新,皮肤2则是等图片下载完成后异步刷新。设置动态换肤,在调用本来的样式设置方法之前会存储该换肤方法的上下文信息,而且存储
key
是方法SEL
,存储值是当前参数,调用同一个换肤方法则会升级存储最新的方法参数,由于对于样式的设置,我们只要关心最后一次的设置即可以了。理论上这种设计是没问题的,但却忽略了一个问题,某些UI组件样式方法的重复调用并不是覆盖设置,而是设置不同状态下的样式,比方UIButton:UIButton *button = [UIButton new];//设置点击图片//[button setImage:image forState:UIControlStateNormal]; [button CJSkinInvokeMethodForSelector:@selector(setImage:forState:) withArguments:@[SkinImageTool(@"按钮"),@(UIControlStateNormal)]]; [button CJSkinInvokeMethodForSelector:@selector(setImage:forState:) withArguments:@[SkinImageTool(@"按钮高亮"),@(UIControlStateHighlighted)]];
这样设置之后,当发生换肤事件后其实UIButton只会升级在高亮状态下的按钮图片。要处理这个问题,我的处理方案是生成存储换肤方法上下文信息的key时,除了记录
SEL
外另外再加上指定的参数信息,这样也就保证了尽管是同一个样式设置方法setImage:forState:
,但却能够存储不同状态下的方法上下文信息。另一种实现方式。针对第3点提到的样式覆盖问题,其实还有更好的处理方案,方案灵感来源于前面讲到的“现有换肤方案分析”—— 基于UIView的分类实现,该方案是在UIView的分类中新添加扩展属性,并通过扩展属性来实现换肤。那同样的我们也可以给 NSObject 的分类添加扩展属性
skinChangeBlock
,skinChangeBlock
是一个代码块,在block代码块内我们可以进行静态换肤样式的设置。当发生换肤,重新执行skinChangeBlock
即可达到动态换肤的效果。而且因为这是 NSObject 的分类,也就意味着除了可以对UIView系列的UI组件进行换肤事件的监听外,其余任意类的实例对象都可以进行换肤设置,这大大添加了换肤框架使用的灵活性。@interface NSObject (Skin)@property (nonatomic, copy) void(^skinChangeBlock)(id weakSelf);@end@implementation NSObject (ZWTSkin)static void *kSkinChangeBlockKey;- (void)setSkinChangeBlock:(void (^)(id))skinChangeBlock { objc_setAssociatedObject(self, &kSkinChangeBlockKey, skinChangeBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); // 判断能否已注册监听换肤通知 if (!_registerSkinNotic) { // 注册监听换肤通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(skinChanged:) name:kSkinDidChangeNotification object:nil]; } __weak typeof(self)wSelf = self; skinChangeBlock(wSelf);}- (void (^)(id))skinChangeBlock { return objc_getAssociatedObject(self, &kSkinChangeBlockKey);}- (void)skinChanged:(NSDictionary *)notic { if (self.skinChangeBlock) { __weak typeof(self)wSelf = self; self.skinChangeBlock(wSelf); }}@end //换肤设置示例UIButton *button = [UIButton new];button.skinChangeBlock = ^(UIButton *weakSelf) { weakSelf.backgroundColor = SkinColor(@"背景色"); [weakSelf setImage:SkinImage(@"按钮") forState:UIControlStateNormal]; [weakSelf setImage:SkinImage(@"按钮高亮") forState:UIControlStateHighlighted];};
写在最后
到此也就已经分析梳理完了iOS APP动态换肤方案的细节实现。主要关键点是将换肤框架拆分为两部分:底层的资源管理层级以及用于监听和管理换肤事件的换肤控制中心,其中换肤控制中心的实现则借用了Object-C所具备的运行时(Runtime)的语言特性。
更多介绍请查看 CJSkin。
假如你有更好的想法,欢迎留言交流。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » APP动态换肤方案详解