衡量一款APP的质量主要由产品的功能、交互、体验决定。在计划开发一款APP时,通常会采用MVP(最简化可实行产品)模式进行,最初的数个版本更多的会将主要精力放在功能和交互上,以最快的速度开发完毕并迅速推向市场,然后基于市场反馈进行迭代,待产品形态趋于稳定后,才会将用户体验作为了一个核心点。


换肤作为一个增强用户体验的功能,现已成为大厂APP的一个标配,包括:

1、局部换肤功能:白天夜间模式的切换,活动日各种图标的更换,字体大小的设置

2、全局换肤功能:皮肤商城主题切换

 

无论是哪种,核心问题都涉及到动态换肤,下面基于这个问题提出了一个通用的解决方案。

一个视图的效果是由布局、资源两部分构成,布局指的是视图的位置,大小;资源指的是颜色、字体、图片、动画等。像H5和Android平台,视图的资源通过配置文件进行独立配置,系统提供了资源自动映射的功能,也就是系统本身支持了资源的动态切换,iOS在这点上需要手动实现了。

 

动态换肤是对视图的资源进行动态更换,涉及到以下几个关键点:

换什么,可更换的资源。基本上人眼可见的都是可以更换的,像颜色、字体、图片、动画等。

如何查找资源,资源包路径、结构和内容,如何映射资源到视图上。

怎么换,资源如何动态设置到视图上。

资源定义

资源包括Color、Font、Image、动画等,系统提供创建这些资源的API是这样的:

UIColor *color = [UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:0.8];

 

UIFont *font = [UIFont systemFontOfSize:14];

 

UIImage *image = [UIImage imageNamed:@"img"];

以Color为例,系统提供创建Color的API中参数都是确定的数字,对于数字是什么我们其实不用太关心,我们需要关心的是这个Color代表的含义,比如导航栏的颜色,那么可以将这个色值映射到导航栏颜色这个key上,这样就可进行配置,进而达到动态替换的目的。

 

最终,我们会将颜色,图片,字体等都通过key值的方式进行设置,结果如下:

//navigationColor=>#EEEEEE

UIColor *color = [UIColor colorWithKey:@"navigationColor"];

 

//navigationTitleFont=>PingFang-SC-Regular size:14

UIFont *font = [UIFont fontWithKey:@"navigationTitleFont"];

 

//navigationBg=>navigationBg.png

UIImage *image = [UIImage imageWithKey:@"navigationBg"];

资源映射

在资源定义好后,需要根据定义的key从对应的皮肤包中找到具体的资源。颜色映射到具体的色值,字体映射到具体的fontName和fontSize,这两者通过plist文件实现即可,图片和动画等直接已对应的文件形式提供即可。最终资源包的结构和内容如下:

接下来,需要进行资源的映射,比如将Color的Key值映射到Color.plist里面对应的Color值。iOS资源包叫Bundle,资源默认是放在mainBundle里,通过获取对应皮肤的Bundle就可以拿到对应的Color.plist文件,进而获取最终的色值。如果是默认皮肤,对应的就是mainBundle,如果切换到另一个皮肤,将该皮肤下载后切换到下载的bundle就可以了。

热切换

假定一个视图在换肤时只需要动态更换一个背景颜色,我们通常会这样做:

//创建并初始化视图,默认color0=>white

UIView *view = [UIView alloc] init];

view.backgroundColor = [UIColor colorWithKey:@"color0"];

 

//切换背景色,重新调用一次设置背景色的方法,color0=>black

view.backgroundColor = [UIColor colorWithKey:@"color0"];

当然,一个APP的换肤功能不可能简单到仅仅只换一处颜色,如果有很多处的话,我们是不可能每一处都重复去调用对应的设置资源的方法的。

 

可以在对视图进行资源设置时,记录下视图、设置资源的方法和所需的参数,然后在动态切换时,将记录的方法自动重复执行即可。iOS提供了NSInvocation类,可以进行方法的动态调用。还是以上面的例子为例,采用NSInvocation,swizzle setBackgroundColor:方法,在调用时可以这样做:

//1、创建NSInvocation

NSMethodSignature *sign = [UIView.class instanceMethodSignatureForSelector:@selector(setBackgroundColor:)];

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sign];

 

//2、保存参数colorKey

//invocation需要参数color,但是此时只有colorKey,在执行具体调用时通过colorKey获取color再进行调用,其他动态参数类似,比如图片,字体等。

 

//3、执行时先通过[UIColor colorWithKey:colorKey]获取Color,设置Invocation color参数

 

//4、调用invocation设置View的背景色

[invocation invokeWithTarget:view];

view会保存它所有的资源设置的invocations,这样在动态切换时循环执行invocations,最终View的资源切换的数据结构如下:

Arg是不可变参数,如UINavigationBar的setBackgroundImage:forBarMetrics:中的barMetrics参数,ArgBuilder用于构建可变参数,比如颜色,字体,动画等等。另外ArgBuilder是链式结构,因为一个可变的参数可能通过多次调用转化而来,比如一张原始图片,需要进行裁剪,拉伸,旋转后才得到最终的图片,那么这个图片需要执行多次ArgBuilder操作,最终才得到结果图片。

面对一个问题时,我们首先需要分析思考其本质,把握住了本质,剩下的就只是coding了。换肤的本质是资源的动态设置,如何定义资源、如何动态设置就是解决这个问题的核心。

来自:点融黑帮(微信号:DianrongMafia),作者:点融·代金波,移动开发工程师,专注移动开发,非典型运动爱好者。