iOS主题切换框架设计(3月7号新更新)

  • 3月7号更新添加了demo,添加了通知移除

本主题切换是基于DKNightVersion修改的,由于DKNightVersion只提供了白天和黑夜两种主题的切换,不符合我们公司的多种主题切换的需求,所以做了一些更改。

demo下载(https://github.com/YasinZhou/METhemeKit)

下图是文件结构供大家参考。
其中ThemeProperties定义了一些key字符串,方便引用。其他的后面都有介绍。

METhemeKit.png

思路

主题的设置无非就是换个颜色、换个图片两种。在设置图片或颜色的时候使用block回调,在回调里面返回这个控件当前主题的图片或颜色。对NSObject进行最底层的属性扩展,添加一个需要做主题切换的属性的数组,保存比如背景颜色需要主题切换和文字颜色需要主题切换等等。在底层的UIColorUIImage使用block进行主题元素动态的绑定,为控件添加一个block属性,利用通知拿到主题的更改,进行重现填充。在上层对各个控件进行封装扩展,添加主题设置方法,比如UIButtonUILable的文字颜色设置可以主题切换。NSObject还添加了一个通知接收方法,接收通知比如UIButtonUILable主题切换了,要重新设置文字颜色。
其中图片主要是根据名字前缀来区分主题。
颜色通过配置文件来读取。

[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添加属性,以及通知的注销。
刚开始写博客没多久,谢谢大家捧场。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,561评论 18 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,977评论 4 60
  • 1 自从微信发明了红包这个功能,越发觉得这是个好东西。 昨天又和男票吵架了,我气坏了,后果很严重。但是最后给我发了...
    山那边海对岸阅读 411评论 0 1
  • 中国传统文化是中华文明演化而汇集成的一种反映民族特质和风貌的民族文化,是民族历史上各种思想文化、观念形态的总体表征...
    高靜娟阅读 702评论 0 0
  • 我开始好像明白我要怎么做了,却又在某一瞬间害怕自己做不到,无论如何,这条路还是要走下去的,不断地挫败也好,那只是人...
    大鹏711阅读 151评论 0 0