自如iOS换肤方案探究

7,189 阅读3分钟

一、前言:

往往到了重大的节假日,例如圣诞节、春节等,各大APP都会进行换肤,烘托喜庆的气氛。购物类APP在618或者双11的时候也会去换上自己的特色服装,找了几个APP分析了一下,大致有以下3种:

  1. 图片资源直接放到APP包里,接口控制是否显示
  2. 接口返回图片的地址,APP根据图片地址去拿图片
  3. 下载压缩包,解压后替换图片 第一种方式会增加APP的包体积,现在为了用户体验,还是尽量不要去给用户增加负担。灵活替换也是一个问题,严重依赖于发版。

第二种方式图片的地址是各自独立的,图片是各自下载,容易出现不完整性的情况,例如tabbar有一张图失败了,那岂不是换肤换了一半。

综合以上考虑,第三种采用压缩包的方式目前来说是比较推荐的。

下面针对我们详细说明一下自如APP的换肤过程。

二、实战

2.1 换肤流程

皮肤的替换流程图如下:

huanfuliuchengtu.png

APP启动后直接加载对应的皮肤文件,同时异步请求后台皮肤接口,接口返回一个压缩包链接,解压后解析包里的config.json文件,然后通过通知去触发换肤。 控制皮肤是否显示的逻辑完全由后台控制,后台返回skinSign为空则关闭换肤.

2.2 实现方式

huanfujiagou2.png

皮肤管理组件分为网络模块、管理模块、Category

我们将皮肤管理器独立Cocoapods组件,业务层依赖换肤组件即可。

下面看一下config.json文件的内容示例:

{
    "home_navi": {
        "colors": {
            "color_background": "#ffffff"
        },
        "images": {
            "image_logo": "home_topLogo"
        }
    },
    "home_tabbar": {
        "colors": {
            "color_background": "#F9F9F9",
            "color_button_normal": "#999999",
            "color_button_selected": "#444444"
        },
        "images": {
            "image_one_button_normal": "tab按钮1图片",
            "image_one_button_selected": "tab按钮1选中图片",
            "image_two_button_normal": "tab按钮2图片",
            "image_two_button_selected": "tab按钮2选中图片",
            "image_three_button_normal": "tab按钮2图片",
            "image_three_button_selected": "tab按钮2选中图片"
        },
        "values": {
            "value_one_button": "tab按钮1",
            "value_two_button": "tab按钮2",
            "value_three_button": "tab按钮3"
        }
    },
    "loading": {
        "resources": {
            "resource_refreshImage" : "refresh.gif"
        } 
    }
}

我们针对首页导航(home_navi)、首页tabbar(home_tabbar)、加载loading(loading)三个模块进行举例。 在每个业务模块下都可以有4个功能模块分别是颜色(colors)、图片(images)、值(values)、资源(resources),这4个模块根据自己的需要进行添加。colors控制的是颜色,这里我以16进制值为准。images控制的是图片,最普通的png文件。values控制的是值。resources控制的是资源文件,例如json、gif等文件。

针对UIView我们创建了一个Category,在这个Category中添加方法,如下:

- (void)configSkinMapModule:(NSString *)module skinMap:(NSDictionary *)skinMap;

- (void)configSkinMapModule:(NSString *)module skinMap:(NSDictionary *)skinMap
{
    if (![ZRSkinManager sharedInstance].isOpenZRSkinManager) {
        return;
    }
    NSMutableDictionary *tempDic = [skinMap mutableCopy];
    for (NSUInteger i = 0; i < tempDic.allKeys.count; i ++) {
        NSString *key = tempDic.allKeys[i];
        NSString *value = tempDic[key];
        tempDic[key] = [NSString stringWithFormat:@"%@.%@",module,value];
    }
    self.skinMap = [tempDic copy];
}

然后我们在需要换肤的模块上注册一下,例如我们给tabbar的第一个按钮添加一下换肤功能,代码如下:

[_tabbarButton configSkinMapModule:kSkin_MODULE_HOME_TABBAR skinMap:
     @{kSkinMapKey_button_image : @"image_one_button_normal",
       kSkinMapKey_button_selectedImage : @"image_one_button_selected",
       kSkinMapKey_button_titleColor : @"color_button_normal",
       kSkinMapKey_button_titleSelectedColor : @"color_button_selected",
       kSkinMapKey_button_title : @"value_one_button"
       }];

执行了以上代码之后发生了什么呢?我会在skinMap的set方法中给此_tabbarButton加上NSNotificationCenter

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(skinChanged) name:kZRSkinDidChangeNotification object:nil];

当要换肤的时候,我们会触发kZRSkinDidChangeNotification通知。

那么skinChanged方法做了哪些操作呢?

我会创建一个SkinConstants文件去定义一下替换的方式标识。

// button相关
static NSString * const kSkinMapKey_button_image = @"kSkinMapKey_button_image";
static NSString * const kSkinMapKey_button_highlightedImage = @"kSkinMapKey_button_highlightedImage";
static NSString * const kSkinMapKey_button_selectedImage = @"kSkinMapKey_button_selectedImage";
static NSString * const kSkinMapKey_button_disabledImage = @"kSkinMapKey_button_disabledImage";
static NSString * const kSkinMapKey_button_titleColor = @"kSkinMapKey_button_titleColor";
static NSString * const kSkinMapKey_button_titleHighlightedColor = @"kSkinMapKey_button_titleHighlightedColor";
static NSString * const kSkinMapKey_button_titleSelectedColor = @"kSkinMapKey_button_titleSelectedColor";
static NSString * const kSkinMapKey_button_titleDisabledColor = @"kSkinMapKey_button_titleDisabledColor";
static NSString * const kSkinMapKey_button_title = @"kSkinMapKey_button_title";

// label相关
static NSString * const kSkinMapKey_label_text = @"kSkinMapKey_label_text";
static NSString * const kSkinMapKey_label_textColor = @"kSkinMapKey_label_textColor";
static NSString * const kSkinMapKey_label_backgroundColor = @"kSkinMapKey_label_backgroundColor";

// imageview相关
static NSString * const kSkinMapKey_imageView_image = @"kSkinMapKey_imageView_image";
static NSString * const kSkinMapKey_imageView_gif = @"kSkinMapKey_imageView_gif"; // gif动画
static NSString * const kSkinMapKey_imageView_backgroundColor = @"kSkinMapKey_imageView_backgroundColor";

相信从名字你们就能看出来,每一个定义都是UIKit里面的一个方法。

然后我说一下刚才那个Category中加的方法,其中module对应的正是config.json中的业务模块,例如home_navi。skinMap中的key是替换的方式标识正是SkinConstants中的定义,value则是config.json中的对应的模块的key值。 也就是上面加的方法的意思是给这个home_navi业务模块中的某一个button增加了修改普通模式图片(kSkinMapKey_button_image)、修改选中模式图片(kSkinMapKey_button_selectedImage)、普通模式文字颜色(kSkinMapKey_button_titleColor)、修改选中模式图片(kSkinMapKey_button_selectedImage)、修改文字值(kSkinMapKey_button_title)的功能。

我们在通知触发方法中使用如下代码去执行替换过程

- (void)changeSkin
{
    NSDictionary *map = self.skinMap;
    if ([self isKindOfClass:[UIButton class]]) {
        UIButton *obj = (UIButton *)self;
        if (map[kSkinMapKey_button_image]) {
            [obj setImage:SkinImage(map[kSkinMapKey_button_image]) forState:UIControlStateNormal];
        }
        if (map[kSkinMapKey_button_highlightedImage]) {
            [obj setImage:SkinImage(map[kSkinMapKey_button_highlightedImage]) forState:UIControlStateHighlighted];
        }
        if (map[kSkinMapKey_button_selectedImage]) {
            [obj setImage:SkinImage(map[kSkinMapKey_button_selectedImage]) forState:UIControlStateSelected];
        }
        if (map[kSkinMapKey_button_disabledImage]) {
            [obj setImage:SkinImage(map[kSkinMapKey_button_disabledImage]) forState:UIControlStateDisabled];
        }
        if (map[kSkinMapKey_button_titleColor]) {
            [obj setTitleColor:SkinColor(map[kSkinMapKey_button_titleColor]) forState:UIControlStateNormal];
        }
      ...以下省略...
}

同时我本地会存有一个localConfig.json用于管理本地的需要替换皮肤的模块,内容和config.json一模一样。只是他取的都是本地默认的皮肤资源配置。 SkinImage是处理images模块的,这个宏定义是pngResourceForSign:方法的宏,用于去处理该加载哪个图片文件。 关于colors、resources等其他模块我就不一一介绍了,都是大同小异。

// 获取Png资源
- (UIImage *)pngResourceForSign:(NSString *)sign;
{
    NSArray *array = [sign componentsSeparatedByString:@"."];
    NSString *module = array.firstObject;
    NSString *key = array.lastObject;
    NSDictionary *moduleDic = self.configData[module];
    NSDictionary *imageDic = moduleDic[@"images"];
    NSString *value = imageDic[key];
    // 这里已经在初始化的时候做了判断,self.path有值则为后台皮肤,无值则为本地默认皮肤。
    if (!self.path.length) {
        return [UIImage imageNamed:value];
    }
    NSString *filePath = [self.path stringByAppendingFormat:@"/%@",value];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    return image;
}

以上就是换肤的核心思路部分,主要就是通过Category的方式,使每一个UIView都拥有换肤的能力,然后通过NSNotificationCenter的方式触发。皮肤下载,皮肤管理等部分就不一一介绍了。

三、结语:

换肤的方式千千万,但基于iOS的特性都离不开Category,如果你还有其他的方案,欢迎一起交流。

参考资料:

  1. github·ThemeManager
  2. github·SwiftTheme
  3. iOS换肤方案
  4. github·EasyTheme
  5. 「节日换肤」通用技术方案__iOS端实现

本文作者:自如大前端研发中心-曲茵

招聘信息

自如大前端研发中心招募新同学!

FE/iOS/Android工程师

公司福利有:

  • 全额五险一金,并额外购买商业保险
  • 免费健身房+年度体检
  • 公司附近租房9折优惠
  • 每年2次晋升机会 办公地点:北京酒仙桥普天实业科技园 欢迎对技术有执着热爱的你加入我们!简历请投递 zhangxl122@ziroom.com, 或加微信 v-nice-v 详聊!