iOS主题切换框架设计(3月7号新更新)
- 3月7号更新添加了demo,添加了通知移除
本主题切换是基于DKNightVersion修改的,由于DKNightVersion只提供了白天和黑夜两种主题的切换,不符合我们公司的多种主题切换的需求,所以做了一些更改。
demo下载(https://github.com/YasinZhou/METhemeKit)
下图是文件结构供大家参考。
其中ThemeProperties定义了一些key字符串,方便引用。其他的后面都有介绍。
思路
主题的设置无非就是换个颜色、换个图片两种。在设置图片或颜色的时候使用block回调,在回调里面返回这个控件当前主题的图片或颜色。对NSObject
进行最底层的属性扩展,添加一个需要做主题切换的属性的数组,保存比如背景颜色需要主题切换和文字颜色需要主题切换等等。在底层的UIColor
和UIImage
使用block进行主题元素动态的绑定,为控件添加一个block属性,利用通知
拿到主题的更改,进行重现填充。在上层对各个控件进行封装扩展,添加主题设置方法,比如UIButton
、UILable
的文字颜色设置可以主题切换。NSObject
还添加了一个通知接收方法,接收通知比如UIButton
、UILable
主题切换了,要重新设置文字颜色。
其中图片主要是根据名字前缀来区分主题。
颜色通过配置文件来读取。
[TOC]
METhemeManager
METhemeManager
作为管理者,提供主题的配置,提供主题的切换(配置参数更新、发送主题切换通知)。
///METhemeManager应该作为单例出现在工程中
+ (METhemeManager *)sharedThemeManager;
///当前主题,以及主题的修改,重写了set方法,set方法里面发送通知
@property (nonatomic,assign) ThemeType themeType;
///当前主题的配置参数
@property (nonatomic, strong,readonly) NSDictionary *currentThemeConfig;
///当前主题图片名字前缀
@property (nonatomic, strong, readonly) NSString *imageNamePrefix;
Config主题配置
-
图片
图片使用前缀进行区分,比如Default的图片名字为buttonImage@2x.png
,新年橙色主题的图片名字就是year_buttonImage@2x.png
,在读取图片的时候根据前缀读取相应的图片,这个前缀是针对每个主题自己定义的。
添加了UIImage
的扩展方法。
///`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 {
//如果根据前缀没有读取到图片,则读取原始图片
return [UIImage imageNamed:name];
}
};
}
-
颜色
使用JSON格式的配置文件,每个配置文件都是单独的配置文件。
使用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里面,感觉耦合性太强,不移维护,这部分还有待优化
通知
METhemeManager
作为管理者发送通知和切换配置是重要的一项
-(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();
}
//保存当前配置到本地
[NeighborUtil saveDataWithKey:ThemeTypeKey ofValue:@(themeType)];
/**
* 发送通知
*/
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"kMEThemeChangeNotificationName" object:nil userInfo:nil];//postNotificationName:kThemeChangeNotification object:nil];
});
}
使用
以UILabel
为例,系统的设置文本颜色的方法是:
我们为`UILabel`扩展里面使用runtime添加一个属性
`@property (nullable,nonatomic,copy)MEColorPicker me_textColor;`
当`UILabel`的`textColor`(文本颜色)需要切换主题时更改颜色,使用新的方法
`priceLabel.me_textColor = [UIColor me_colorPickerForMode:ThemeColorMode_Default];`
`setMe_textColor`的实现里面我们会调用`UILabel`的`setTextColor`方法设置文本颜色,同时会把`setTextColor`方法记录在`NSObject`的需要做主题切换时重新调用的方法的数组里面,在接收到切换主题的通知时重新调用`setTextColor`方法。MEColorPicker是一个颜色block,可以获取到当前主题的颜色配置,下面会介绍这个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);
//调用原始的方法
self.textColor = me_textColor();
//保存主题填充的操作,将(MEColorPicker)me_textColor参数和"setTextColor:"方法绑定保存
[self.pickers setValue:[me_textColor copy] forKey:@"setTextColor:"];
}
@end
```
###核心
+ block
通过block的回调拿到相应配置的参数
```
//颜色Block
typedef UIColor *(^MEColorPicker)(void);
```
```
//图片Block
typedef UIImage *(^MEImagePicker)(void);
```
通过这两种回调会拿到当前主题的颜色和图片。
+ NSObject扩展
下图可以看出所有的控件都是`NSObject`作为底层类,我们可以对`NSObject`进行扩展,增加参数来保存Block和接收通知,然后再去对各种控件进行扩展,改写颜色或者图片的赋值方法(根据自己的需要进行添加)
![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);
```
```
//添加参数来保存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) {
//获取数组的时候进行初始化操作,同时进行通知的注册
if (self.deallocHelperExecutor == nil) {
//这里添加一个属性,监听控件的dealloc事件,进行通知的移除
__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`为例,我们在设置`textColor`(文本颜色)的时候,调用了`UILabel`的新属性`me_textColor`,把`me_textColor`(颜色block,可以获取到当前主题的颜色配置)参数和`setTextColor:`方法保存在`pickers`中,在接到通知的时候会从`pickers`中拿到`me_textColor`参数和`setTextColor:`方法,重新调用`self.textColor = me_textColor();`即可完成主题的变更
* 通知的注销
通知的移除应该在`dealloc`方法里面进行,但是由于扩展没办法重写类的方法,所以就调取不到dealloc事件,所以给`NSObject`添加一个参数,在NSObject释放的时候这个参数也会释放,在这个参数的dealloc方法里面移除NSObject的通知。
参数也添加在一个新的扩展里面`NSObject+DeallocBlock.h`
```
@interface NSObject (DeallocBlock)
/*
deallocHelperExecutor是一个继承于NSObject的类,主要作用就是使用它的dealloc事件移除通知
*/
@property (nonatomic, copy)MEDeallocBlockExecutor *deallocHelperExecutor;
@end
```
在初始化pickers的时候添加deallocHelperExecutor参数,MEDeallocBlockExecutor类保存一个回调,在dealloc里面调用回调
```
- (instancetype)initWith:(DeallocBlock)deallocBlock{
self = [super init];
if (self) {
_deallocBlock = [deallocBlock copy];
}
return self;
}
-(void)dealloc{
if (self.deallocBlock) {
self.deallocBlock();
}
}
```
###解耦
控件的配置读取放在控件的扩展里面去做,尽量的做到各自的配置各自管理
比如`UIButton`,我们可以配置确定按钮配置、取消按钮配置、返回按钮配置,并且配置的每种点击状态都不一样
```
"Button":{
"ThemeMode_Button_NoBackgroundImage_SureButton": {
"titleColor": {
"UIControlStateNormal":"F85825",
"UIControlStateHighlight":"F85825",
"UIControlStateDisabled":"666666",
"UIControlStateSelected":"F85825"
}
},
"ThemeMode_Button_NavBarRight":{
"titleColor": {
"UIControlStateNormal":"F85825",
"UIControlStateHighlight":"4DF85825",
"UIControlStateDisabled":"7FF85825"
}
}
}
```
在设置`titleColor`的时候,我们需要从配置文件中读取到一个颜色block,如果你的`UIButton`每个`UIControlState`(点击状态)都有一个颜色设置,这个`MEColorPicker的获取`不要放在`UIColor`扩展里面,应该放在`UIButton`扩展里面。
```
-(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的颜色设置,而且和主题的大色彩是一样的,可以直接调用`me_setTitleColor: forState:`picke从`UIColor` 中读取,比如
```
[newButton me_setTitleColor:[UIColor me_colorPickerForMode:@"ThemeColorMode_Default"] forState:UIControlStateNormal];
```
`me_colorPickerForMode:`的代码在上面颜色配置的介绍地方。
####结尾
由于主题的配置各个公司可能会有各自的需求,所以我这里提供的是一种设计思路,具体的实现可以参照demo自己重新写一套自己项目的主题框架。比如配置文件格式、控件扩展的方法(需要用到哪个方法就改写哪个方法)。核心还是block和runtime添加属性,以及通知的注销。
刚开始写博客没多久,谢谢大家捧场。