iOS多组件App皮肤配置方案及实践

应用换肤是常见的需求了,项目工程需要将设计资源的颜色、字体、图片等设计元素使用皮肤文件进行配置管理。本方案主要以项目支持马甲包资源管理、样式配置为主。

核心需求点:

  1. 马甲包开发支持,方便马甲包进行样式配置(区分)及资源包集成;

  2. 皮肤包功能,支持动态下载皮肤包(比如节日包),并应用到App上;

  3. 皮肤热更新,支持后台推送、App实时更新样式;

优先支持需求点1、2,需求点2、需求点3需后台配合支持。

代码开发快速上手请见「[配置格式说明]」及「[快速上手]」。代码仓库在HTSkinBundle

样式配置

配置格式说明

目前使用json作为配置文件格式。

目前暂定,应用全局配置使用主皮肤包中的appstyle.json,业务配置可自定somemodule.json。应用全局配置具有较为固定的key、默认会在App启动后读取配置应用到全局UI上,业务配置按需自定。

分割为多个配置文件,一则减少单次IO访问阻塞时间,二则隔离各业务配置、减少冲突。

以下为示例配置文件。每个key是业务id,也就是带义务意义的key。单个配置文件内应保证key的唯一性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"primaryGradientColor":{
"colors":[
"#160226",
"#280842"
],
"locations":[0, 1],
"style":0
},
"next_button_title_color":["#8A76FC", "#FF52D3", "#F2B273"],
"next_button_title_font":{
"fontName":"PingFangSC-SemiBold",
"fontSize":18
},
"some_other_font": "PingFangSC-Medium,16",
"some_bg_image_in_assets": "bg_home",
"some_bg_image_single": "bg_im_head_superplayer",

"test_title_color_plain":"#333333"
}
颜色配置格式

颜色的配置格式包括三种:

  1. String

纯色配置。支持#RGB、0xRGBA、#RRGGBB、RRGGBBAA等格式的颜色配置,与UIColor+YYAdd.h保持一致。通常情况默认使用#RRGGBB即可。

1
2
3
{
"test_title_color_plain":"#333333"
}

因UI规范未定,后续可能有变动。

  1. Object

渐变色配置。主要包括3个key的配置:

  • colors: Array,元素同上述1的String格式

  • locations: Array,元素为Float格式,取值范围[0, 1]

  • styleInt,可选0/1/2/3,请参考GradientStyle的取值,可拓展。

属性意义可参考CAGradientLayer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"next_button_title_color":{
"colors":[
"#8A76FC",
"#FF52D3",
"#F2B273"
],
"locations":[
0,
0.51,
1
],
"style":0
}
}
  1. Array

渐变色配置,即第2种格式的简化版,只使用colors进行配置。默认locations为均分,即均匀渐变,style为GradientStyleLeftToRight。

字体配置格式

字体的配置格式包括三种:

  1. Float

通过字号配置,将使用皮肤包「全局配置」中的字体(或系统字体)。

⚠️请注意是Float格式,若配置为String格式,运行期Debug模式会断言失败、提示修改。

1
2
3
{
"navigationTitleFont":17
}
  1. Object

可指定三个key进行配置:

  • fontName: String 字体名称

  • fontSize: Float 字号大小,必选

  • fontWeight: String 字重,可选:light/regular/medium/semibold/bold

请注意fontSize为必要的配置。若不提供fontName则同上述1处理。

1
2
3
4
5
6
{
"next_button_title_font":{
"fontName":"PingFangSC-SemiBold",
"fontSize":18
}
}

⚠️请注意fontWeight 字重仅为系统字体提供,约等同于[UIFont boldSystemFontOfSize:$size]此类用途。

  1. String

提供便捷的配置格式,字体名称及字号大小,以逗号,分割,示例如”PingFangSC-Medium,16”。

⚠️请注意需要名称、字号同时配置,如仅配置”16”字号会断言失败。

默认情况下,App使用皮肤包配置的全局字体,因此业务如无特别需求,使用第一种方式指定字号即可。

图片配置格式

图片的配置目前仅一种,key: imageName

其他配置

其他不属于资源配置,但可能增加接口支持的,比如某些常见的UI控件的配置属性,主要是统一、方便使用。

待定。

全局样式配置

全局样式主要包括App主色、默认字体、导航栏等全局性页面容器等外观配置。

App主色

对应视觉UI规范的主色配置,配置文件中由primaryColor指定,纯色。

App默认字体

配置文件中的primaryFont进行指定。支持加载皮肤包携带字体,指定字体名称以在App中全局配置使用(代码需配置),比如可以从PingFang字体切换为自定义字体等。

请确保字体为系统字体,或App内置字体,或皮肤包内置字体。皮肤包内置字体可通过”appstyle.json”配置fonts指定需加载的字体文件。

皮肤包加载时,默认会搜索皮肤包中ttf后缀的字体进行加载。

若不指定primaryFont,则使用系统默认字体。

导航栏

导航栏的视觉配置,主要包括:(待定)

  1. 导航栏背景色/图片:navigationColornavigationBgImage

  2. 导航栏标题颜色:navigationTitleColor

  3. 导航栏标题字体:navigationTitleFont

  4. 导航栏返回按钮图片:navigationBackImage

见下方json配置文件定义。

tabBar

tabBar的视觉配置,主要包括:(待定)

  1. tabBar背景色/图片

  2. tabBar文案字体

  3. tabBar文案颜色

建议修改或开发TabBarController,以支持由内容页面提供响应tab的tabBarIcon图片及文案。

皮肤包中全局配置文件(appstyle.json)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"primaryFont":"HYLiLiangHeiJ",
"primaryColor":"#4594FF",
"primaryGradientColor":[
"#8A76FC",
"#FF52D3",
"#F2B273"
],
"registerFonts":[
"HYLiLiangHeiJ"
],
"navigationColor":"#4594FF",
"navigationTitleFont":17,
"navigationTitleColor":"#FFFFFF",
"navigationBgImage":"img_nav_bg",
"navigationBackImage":"img_nav_back",
"badgeColor":"#FF6365"
}

registerFonts是配置需要动态注册的字体。也可不配置,程序可配置自动搜索皮肤包中的ttf字体文件进行加载。

全局配置文件支持拓展,但通常情况会进行字段限定(比如与设计出的规范文档对齐),并保持稳定。

业务定制样式配置

默认支持(或可支持)UIView及常见控件的样式属性可配置。特殊需支持的配置需求,需要控件支持配置,如未支持需安排开发。

目前GradientButtonGradientLabel已提供渐变背景、渐变文字支持。

皮肤包资源构成

皮肤包的配置及资源,在HTSkinBundle组件中可以配置在Assets目录下。

如下图示是以项目区分的资源构成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
├── Assets
│ ├── Common
│ ├── App1
│ │ ├── LottieResource
│ │ │ └── bingoLottie
│ │ │ ├── data.json
│ │ │ └── images
│ │ │ ├── img_0.png
│ │ │ ├── img_1.png
│ │ │ ├── img_2.png
│ │ │ ├── img_3.png
│ │ │ ├── img_4.png
│ │ │ ├── img_5.png
│ │ │ ├── img_6.png
│ │ │ ├── img_7.png
│ │ │ ├── img_8.png
│ │ │ └── img_9.png
│ │ ├── Skin.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── bg_home.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── bg_home@2x.png
│ │ │ │ └── bg_home@3x.png
│ │ │ └── bg_home_filter.imageset
│ │ │ ├── Contents.json
│ │ │ ├── bg_home_filter@2x.png
│ │ │ └── bg_home_filter@3x.png
│ │ ├── appstyle.json
│ │ ├── images
│ │ │ ├── bg_im_head_superplayer.png
│ │ │ └── warning.png
│ │ └── login.json
│ ├── App2
│ └── App3

资源

皮肤资源包括字体文件(ttf)、图片(png/jpg)、图片集xcassets、Lottie资源、音频mp3,以及皮肤包的配置文件,都属于皮肤包资源。

目前支持集成的文件后缀包括:xib,png,xcassets,svg,ttf,json,xml,plist,mp3。如果新增需要更新皮肤包组件描述。

共有资源

考虑到目前项目间可能存在大量相同的资源,资源包支持在将其放置在Common目录下,目录结构及文件类型支持保持一致。非必要配置。

项目特有资源

皮肤包工程可自定义项目资源目录,现有App1/App2/App3等(如上目录结构树所示)。

皮肤包最终会以Skin.bundle资源包的形式集成到主项目中,一般情况下在.app路径下。

图片可以添加在xcassets资源集或images目录下。images目录可以划分目录管理图片资源。

皮肤包集成

基于pod组件进行集成。使用方式说明请见「快速上手」部分。

主皮肤包

跟随HTSkinBundle进行集成即可。

组件资源包

此组件,特指项目的Component类组件,通常为业务组件。业务组件如需要区分马甲包进行样式配置,可以添加配置文件到组件资源包中,业务上使用方法除指定component外几乎一致。

若业务组件需要区分资源包,请业务组件自行安排集成,比如通过subspec方式来区分项目集成亦可。

皮肤包更新

需要将主皮肤包映射到沙盒中。支持全量更新或增量更新,具体策略,涉及资源包版本管理、补丁包制作及更新包等细化逻辑。以下说明方案思路。

资源包更新

管理后台

管理后台支持资源包上传(托管)、版本化及差分包生成等。主要为后端内容,差分/补丁制作工具与客户端保持统一即可,此处不赘述。

皮肤包版本约束包括主应用最低版本要求、主皮肤包版本、差分包版本及对应主皮肤包版本等版本信息。

全量更新

主要流程:

  1. 皮肤包zip上传到文件托管服务;

  2. 客户端与后台进行配置版本同步,同步策略两端协商即可。比如可以App定期启动后拉取配置版本,也可以服务端推送或其他手段进行同步;

  3. 客户端版本比对能力,客户端按需下载皮肤包文件;

  4. 客户端更新皮肤包映射关系,触发皮肤热更新等;

增量更新

增量更新包括两部分:差分包(补丁包)制作、差分包应用(集成)。

  1. 差分包制作

差分包的制作需要使用diff工具,具体的选择看需要。目前bsdiff和bspatch是一套常用的工具,可以参考使用。考虑进一步性能优化情况下,可再考虑其他diff工具的组合使用。

  1. 差分包集成

通常是阶梯式进行diff应用。

  • 客户端在拉取到皮肤包配置后,根据应用版本及资源版本自行判断是否适用全量更新或增量更新;

  • 确定增量更新后,递归下载所需的增量包;

  • 递归、阶梯式应用增量包到全量包生成新的全量包,直到生成目标资源包结束;

  • 客户端更新皮肤包映射关系,并触发皮肤热更新。

皮肤热更新

包含两部分:

  1. 热切换

主要是指客户端需要支持配置更新(全部或个别)后,在App侧能直接生效。具体实现方式,可通过观察者模式或闭包缓存的方式实现。

对业务开发使用无影响。

  1. 热更新

主要是指客户端需要及时获知配置及资源包的更新,比如轮询,比如服务端推送等多种手段都可实现,此处亦不赘述。

快速上手

  1. Podfile添加依赖HTSkinBundle

默认集成项目的资源包,相关项目或希望调试其他资源包时,需指定对应的subspec

1
2
pod 'HTSkinBundle'
pod 'HTSkinBundleResource'

或有需要可以将资源包抽离为独立的pod组件,将资源托管到静态服务。

  1. 使用支持配置的控件
1
2
3
4
5
6
7
8
9
10
11
import HTSkinBundle
fun test() {
let button = GradientButton()
button.layer.cornerRadius = 60/2
button.layer.masksToBounds = true
button.style.module("login")
.backgroundColorKey("next_button_bg_color")
.textColorKey("next_button_title_color")
.fontKey("next_button_title_font")
button.textLabel.text = "测试测试测试"
}

Swift版的接口,控件的可配置属性约束在style的域下所支持的属性。

目前对UIViewUILabel等基础控件进行属性配置的支持(持续更新中)。

定制配置目前主要包括GradientButtonGradientLabel主要提供渐变背景及渐变文字的支持。

Comments