自如iOS換膚方案探究
一、前言:
往往到了重大的節假日,例如聖誕節、春節等,各大APP都會進行換膚,烘托喜慶的氣氛。購物類APP在618或者雙11的時候也會去換上自己的特色服裝,找了幾個APP分析了一下,大致有以下3種:
- 圖片資源直接放到APP包裏,接口控制是否顯示
- 接口返回圖片的地址,APP根據圖片地址去拿圖片
- 下載壓縮包,解壓後替換圖片 第一種方式會增加APP的包體積,現在為了用户體驗,還是儘量不要去給用户增加負擔。靈活替換也是一個問題,嚴重依賴於發版。
第二種方式圖片的地址是各自獨立的,圖片是各自下載,容易出現不完整性的情況,例如tabbar有一張圖失敗了,那豈不是換膚換了一半。
綜合以上考慮,第三種採用壓縮包的方式目前來説是比較推薦的。
下面針對我們詳細説明一下自如APP的換膚過程。
二、實戰
2.1 換膚流程
皮膚的替換流程圖如下:
APP啟動後直接加載對應的皮膚文件,同時異步請求後台皮膚接口,接口返回一個壓縮包鏈接,解壓後解析包裏的config.json文件,然後通過通知去觸發換膚。 控制皮膚是否顯示的邏輯完全由後台控制,後台返回skinSign為空則關閉換膚.
2.2 實現方式
皮膚管理組件分為網絡模塊、管理模塊、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次晉升機會 辦公地點:北京酒仙橋普天實業科技園 歡迎對技術有執着熱愛的你加入我們!簡歷請投遞 [email protected], 或加微信 v-nice-v 詳聊!