- 3月7號更新添加了demo妇智,添加了通知移除
本主題切換是基于DKNightVersion修改的终议,由于DKNightVersion只提供了白天和黑夜兩種主題的切換,不符合我們公司的多種主題切換的需求主届,所以做了一些更改另锋。
demo下載(https://github.com/YasinZhou/METhemeKit)
下圖是文件結(jié)構(gòu)供大家參考。
其中ThemeProperties定義了一些key字符串狈惫,方便引用睛蛛。其他的后面都有介紹。
思路
主題的設(shè)置無非就是換個顏色、換個圖片兩種忆肾。在設(shè)置圖片或顏色的時候使用block回調(diào)菠红,在回調(diào)里面返回這個控件當(dāng)前主題的圖片或顏色。對NSObject
進(jìn)行最底層的屬性擴(kuò)展难菌,添加一個需要做主題切換的屬性的數(shù)組试溯,保存比如背景顏色需要主題切換和文字顏色需要主題切換等等。在底層的UIColor
和UIImage
使用block進(jìn)行主題元素動態(tài)的綁定郊酒,為控件添加一個block屬性遇绞,利用通知
拿到主題的更改,進(jìn)行重現(xiàn)填充燎窘。在上層對各個控件進(jìn)行封裝擴(kuò)展摹闽,添加主題設(shè)置方法,比如UIButton
褐健、UILable
的文字顏色設(shè)置可以主題切換付鹿。NSObject
還添加了一個通知接收方法,接收通知比如UIButton
蚜迅、UILable
主題切換了舵匾,要重新設(shè)置文字顏色。
其中圖片主要是根據(jù)名字前綴來區(qū)分主題谁不。
顏色通過配置文件來讀取坐梯。
[TOC]
METhemeManager
METhemeManager
作為管理者,提供主題的配置刹帕,提供主題的切換(配置參數(shù)更新吵血、發(fā)送主題切換通知)。
///METhemeManager應(yīng)該作為單例出現(xiàn)在工程中
+ (METhemeManager *)sharedThemeManager;
///當(dāng)前主題偷溺,以及主題的修改蹋辅,重寫了set方法,set方法里面發(fā)送通知
@property (nonatomic,assign) ThemeType themeType;
///當(dāng)前主題的配置參數(shù)
@property (nonatomic, strong,readonly) NSDictionary *currentThemeConfig;
///當(dāng)前主題圖片名字前綴
@property (nonatomic, strong, readonly) NSString *imageNamePrefix;
Config主題配置
-
圖片
圖片使用前綴進(jìn)行區(qū)分挫掏,比如Default的圖片名字為buttonImage@2x.png
,新年橙色主題的圖片名字就是year_buttonImage@2x.png
,在讀取圖片的時候根據(jù)前綴讀取相應(yīng)的圖片侦另,這個前綴是針對每個主題自己定義的。
添加了UIImage
的擴(kuò)展方法砍濒。
///`UIImage+Theme.h`
+(MEImagePicker)me_imageNamed:(NSString *)name {
return ^() {
//獲得主題圖片名字前綴淋肾,比如“”和“year_”兩種主題名字前綴
NSString *pre = [METhemeManager getImageNamePrefix];
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%@%@",pre,name]];
if (image) {
return image;
}else {
//如果根據(jù)前綴沒有讀取到圖片硫麻,則讀取原始圖片
return [UIImage imageNamed:name];
}
};
}
-
顏色
使用JSON格式的配置文件爸邢,每個配置文件都是單獨(dú)的配置文件。
使用JSON而非plist的原因是格式是key-value拿愧,而非xml杠河,編寫更加方便,錯誤易查、易改券敌。
//基本Color的配置
"Color":{
"ThemeColorMode_Default":"F85825",
"ThemeColorMode_Default_Highlight":"D34D21"
}
//針對控件的配置
"UINavigationBar": {
"NavBarDefault":{
"tintColor":"FF6E40",
"backgroundImageColor":"FF6E40",
"shadowImageColor":"00",
"titleLabelColor":"FFFFFF"
},
"NavBarLevel1":{
"tintColor":"F8F8F8",
"backgroundImageColor":"F8F8F8",
"shadowImageColor":"4DB2B2B2",
"titleLabelColor":"333333"
}
}
//獲取顏色配置:
+(MEColorPicker)me_colorPickerForMode:(NSString *)mode {
return ^() {
NSString *colorHexStr = [self getColorForMode:mode];
return [self me_colorWithHexString:colorHexStr];
};
}
+(NSString *)getColorForMode:(NSString *)mode {
NSString *colorStr = [METhemeManager sharedThemeManager].currentThemeConfig[@"Color"][mode];
return colorStr;
}
** 控件的配置也在JSON里面唾戚,感覺耦合性太強(qiáng),不移維護(hù)待诅,這部分還有待優(yōu)化
通知
METhemeManager
作為管理者發(fā)送通知和切換配置是重要的一項(xiàng)
-(void)setThemeType:(ThemeType)themeType {
_themeType = themeType;
NSString *path;
switch (themeType) {
case ThemeDefault:{
_imagePreStr = @"";
path = [[NSBundle mainBundle]pathForResource:@"ThemeDefault" ofType:@"json"];
}
break;
case ThemeYear:{
_imagePreStr = @"year_";
path = [[NSBundle mainBundle]pathForResource:@"ThemeOrange" ofType:@"json"];
}
break;
default:
break;
}
NSData *jsonData = [NSData dataWithContentsOfFile:path];
_currentThemeConfig = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:nil];
if (_currentThemeConfig == nil) {
NSAssert(false, @"ThemeConfig配置有誤", self);
abort();
}
//保存當(dāng)前配置到本地
[NeighborUtil saveDataWithKey:ThemeTypeKey ofValue:@(themeType)];
/**
* 發(fā)送通知
*/
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"kMEThemeChangeNotificationName" object:nil userInfo:nil];//postNotificationName:kThemeChangeNotification object:nil];
});
}
使用
以UILabel
為例叹坦,系統(tǒng)的設(shè)置文本顏色的方法是:
我們?yōu)閌UILabel`擴(kuò)展里面使用runtime添加一個屬性
`@property (nullable,nonatomic,copy)MEColorPicker me_textColor;`
當(dāng)`UILabel`的`textColor`(文本顏色)需要切換主題時更改顏色,使用新的方法
`priceLabel.me_textColor = [UIColor me_colorPickerForMode:ThemeColorMode_Default];`
`setMe_textColor`的實(shí)現(xiàn)里面我們會調(diào)用`UILabel`的`setTextColor`方法設(shè)置文本顏色卑雁,同時會把`setTextColor`方法記錄在`NSObject`的需要做主題切換時重新調(diào)用的方法的數(shù)組里面募书,在接收到切換主題的通知時重新調(diào)用`setTextColor`方法。MEColorPicker是一個顏色block测蹲,可以獲取到當(dāng)前主題的顏色配置莹捡,下面會介紹這個block。
```
#import "UILabel+Theme.h"
#import "NSObject+Theme.h"
#import <objc/runtime.h>
@implementation UILabel (Theme)
-(MEColorPicker)me_textColor{
return objc_getAssociatedObject(self, @selector(me_textColor));
}
-(void)setMe_textColor:(MEColorPicker)me_textColor{
//注冊新屬性的set方法
objc_setAssociatedObject(self, @selector(me_textColor), me_textColor, OBJC_ASSOCIATION_COPY_NONATOMIC);
//調(diào)用原始的方法
self.textColor = me_textColor();
//保存主題填充的操作扣甲,將(MEColorPicker)me_textColor參數(shù)和"setTextColor:"方法綁定保存
[self.pickers setValue:[me_textColor copy] forKey:@"setTextColor:"];
}
@end
```
###核心
+ block
通過block的回調(diào)拿到相應(yīng)配置的參數(shù)
```
//顏色Block
typedef UIColor *(^MEColorPicker)(void);
```
```
//圖片Block
typedef UIImage *(^MEImagePicker)(void);
```
通過這兩種回調(diào)會拿到當(dāng)前主題的顏色和圖片篮赢。
+ NSObject擴(kuò)展
下圖可以看出所有的控件都是`NSObject`作為底層類,我們可以對`NSObject`進(jìn)行擴(kuò)展琉挖,增加參數(shù)來保存Block和接收通知启泣,然后再去對各種控件進(jìn)行擴(kuò)展,改寫顏色或者圖片的賦值方法(根據(jù)自己的需要進(jìn)行添加)
![NSObject的繼承圖譜](http://upload-images.jianshu.io/upload_images/1024259-edeaf5331754bfc0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
+ 保存Block
使用`runtime`來為`NSObject`添加一個字典來保存主題填充操作
```
#import <objc/runtime.h> //引入runtime框架
```
```
//MEColorPicker示辈、MEImagePicker...
typedef id _Nullable (^MEPicker)(void);
```
```
//添加參數(shù)來保存MEPicker
@property (nonatomic, strong, nonnull,readonly) NSMutableDictionary<NSString *, MEPicker> *pickers;
```
```
-(NSMutableDictionary<NSString *,MEPicker> *)pickers{
NSMutableDictionary<NSString *,MEPicker> *pickers = objc_getAssociatedObject(self, @selector(pickers));
if (!pickers) {
//獲取數(shù)組的時候進(jìn)行初始化操作种远,同時進(jìn)行通知的注冊
if (self.deallocHelperExecutor == nil) {
//這里添加一個屬性,監(jiān)聽控件的dealloc事件顽耳,進(jìn)行通知的移除
__weak typeof(self) weakSelf;
MEDeallocBlockExecutor *deallocHelper = [[MEDeallocBlockExecutor alloc]initWith:^{
[[NSNotificationCenter defaultCenter] removeObserver:weakSelf];
}];
self.deallocHelperExecutor = deallocHelper;
}
pickers = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self, @selector(pickers), pickers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//初始化的時候添加通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeTheme) name:kMEThemeChangeNotification object:nil];
}
return pickers;
}
```
+ 接收通知后處理
```
-(void)changeTheme {
[self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MEPicker _Nonnull obj, BOOL * _Nonnull stop) {
SEL sel = NSSelectorFromString(key);
id result = obj();
[UIView animateWithDuration:METhemeAnimationDuration animations:^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:sel]) {
[self performSelector:sel withObject:result];
}
#pragma clang diagnostic pop
}];
}
```
* 控件通過KVC向父類`NSObject`的`pickers`添加主題配置
以`UILabel`為例坠敷,我們在設(shè)置`textColor`(文本顏色)的時候,調(diào)用了`UILabel`的新屬性`me_textColor`,把`me_textColor`(顏色block射富,可以獲取到當(dāng)前主題的顏色配置)參數(shù)和`setTextColor:`方法保存在`pickers`中膝迎,在接到通知的時候會從`pickers`中拿到`me_textColor`參數(shù)和`setTextColor:`方法,重新調(diào)用`self.textColor = me_textColor();`即可完成主題的變更
* 通知的注銷
通知的移除應(yīng)該在`dealloc`方法里面進(jìn)行胰耗,但是由于擴(kuò)展沒辦法重寫類的方法限次,所以就調(diào)取不到dealloc事件,所以給`NSObject`添加一個參數(shù)柴灯,在NSObject釋放的時候這個參數(shù)也會釋放卖漫,在這個參數(shù)的dealloc方法里面移除NSObject的通知。
參數(shù)也添加在一個新的擴(kuò)展里面`NSObject+DeallocBlock.h`
```
@interface NSObject (DeallocBlock)
/*
deallocHelperExecutor是一個繼承于NSObject的類赠群,主要作用就是使用它的dealloc事件移除通知
*/
@property (nonatomic, copy)MEDeallocBlockExecutor *deallocHelperExecutor;
@end
```
在初始化pickers的時候添加deallocHelperExecutor參數(shù)羊始,MEDeallocBlockExecutor類保存一個回調(diào),在dealloc里面調(diào)用回調(diào)
```
- (instancetype)initWith:(DeallocBlock)deallocBlock{
self = [super init];
if (self) {
_deallocBlock = [deallocBlock copy];
}
return self;
}
-(void)dealloc{
if (self.deallocBlock) {
self.deallocBlock();
}
}
```
###解耦
控件的配置讀取放在控件的擴(kuò)展里面去做查描,盡量的做到各自的配置各自管理
比如`UIButton`突委,我們可以配置確定按鈕配置柏卤、取消按鈕配置、返回按鈕配置匀油,并且配置的每種點(diǎn)擊狀態(tài)都不一樣
```
"Button":{
"ThemeMode_Button_NoBackgroundImage_SureButton": {
"titleColor": {
"UIControlStateNormal":"F85825",
"UIControlStateHighlight":"F85825",
"UIControlStateDisabled":"666666",
"UIControlStateSelected":"F85825"
}
},
"ThemeMode_Button_NavBarRight":{
"titleColor": {
"UIControlStateNormal":"F85825",
"UIControlStateHighlight":"4DF85825",
"UIControlStateDisabled":"7FF85825"
}
}
}
```
在設(shè)置`titleColor`的時候缘缚,我們需要從配置文件中讀取到一個顏色block,如果你的`UIButton`每個`UIControlState`(點(diǎn)擊狀態(tài))都有一個顏色設(shè)置敌蚜,這個`MEColorPicker的獲取`不要放在`UIColor`擴(kuò)展里面桥滨,應(yīng)該放在`UIButton`擴(kuò)展里面。
```
-(void)me_ButtonTitleColorForMode:(NSString *)mode withState:(UIControlState)state{
MEColorPicker colorPicker = [self getButtonTitleColorForMode:mode withState:state];
[self me_setTitleColor:colorPicker forState:state];
}
-(MEColorPicker)getButtonTitleColorForMode:(NSString *)mode withState:(UIControlState)state{
return ^() {
NSString *colorHexStr = [METhemeManager sharedThemeManager].currentThemeConfig[@"Button"][mode][@"titleColor"][[self buttonControlStateToStr:state]];
UIColor *color = [UIColor me_colorWithHexString:colorHexStr];
if (color == nil) {
color = [self titleColorForState:state];
}
return color;
};
}
- (void)me_setTitleColor:(_Nullable MEColorPicker)picker forState:(UIControlState)state{
[self setTitleColor:picker() forState:state];
NSString *key = NSStringFromSelector(@selector(setTitleColor:forState:));
id dictionary = [self.pickers valueForKey:key];
if (!dictionary || ![dictionary isKindOfClass:[NSMutableDictionary class]]) {
dictionary = [[NSMutableDictionary alloc] init];
}
[dictionary setValue:[picker copy] forKey:[NSString stringWithFormat:@"%@", @(state)]];
[self.pickers setValue:dictionary forKey:key];
}
```
使用的時候如下:
```
[sureButton me_ButtonTitleColorForMode:@"ThemeMode_Button_NoBackgroundImage_SureButton" withState:UIControlStateNormal];
[sureButton me_ButtonTitleColorForMode:@"ThemeMode_Button_NoBackgroundImage_SureButton" withState:UIControlStateHighlighted];
[sureButton me_ButtonTitleColorForMode:@"ThemeMode_Button_NoBackgroundImage_SureButton" withState:UIControlStateDisabled];
[sureButton me_ButtonTitleColorForMode:@"ThemeMode_Button_NoBackgroundImage_SureButton" withState:UIControlStateSelected];
```
除非你的`UIButton`只有一種Normal的顏色設(shè)置弛车,而且和主題的大色彩是一樣的,可以直接調(diào)用`me_setTitleColor: forState:`picke從`UIColor` 中讀取该园,比如
```
[newButton me_setTitleColor:[UIColor me_colorPickerForMode:@"ThemeColorMode_Default"] forState:UIControlStateNormal];
```
`me_colorPickerForMode:`的代碼在上面顏色配置的介紹地方。
####結(jié)尾
由于主題的配置各個公司可能會有各自的需求帅韧,所以我這里提供的是一種設(shè)計思路里初,具體的實(shí)現(xiàn)可以參照demo自己重新寫一套自己項(xiàng)目的主題框架。比如配置文件格式忽舟、控件擴(kuò)展的方法(需要用到哪個方法就改寫哪個方法)双妨。核心還是block和runtime添加屬性,以及通知的注銷叮阅。
剛開始寫博客沒多久刁品,謝謝大家捧場。