iOS主題切換框架設(shè)計(3月7號新更新)

  • 3月7號更新添加了demo妇智,添加了通知移除

本主題切換是基于DKNightVersion修改的终议,由于DKNightVersion只提供了白天和黑夜兩種主題的切換,不符合我們公司的多種主題切換的需求主届,所以做了一些更改另锋。

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

下圖是文件結(jié)構(gòu)供大家參考。
其中ThemeProperties定義了一些key字符串狈惫,方便引用睛蛛。其他的后面都有介紹。

METhemeKit.png

思路

主題的設(shè)置無非就是換個顏色、換個圖片兩種忆肾。在設(shè)置圖片或顏色的時候使用block回調(diào)菠红,在回調(diào)里面返回這個控件當(dāng)前主題的圖片或顏色。對NSObject進(jìn)行最底層的屬性擴(kuò)展难菌,添加一個需要做主題切換的屬性的數(shù)組试溯,保存比如背景顏色需要主題切換和文字顏色需要主題切換等等。在底層的UIColorUIImage使用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添加屬性,以及通知的注銷叮阅。
剛開始寫博客沒多久刁品,謝謝大家捧場。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浩姥,一起剝皮案震驚了整個濱河市挑随,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌勒叠,老刑警劉巖兜挨,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異眯分,居然都是意外死亡拌汇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門弊决,熙熙樓的掌柜王于貴愁眉苦臉地迎上來噪舀,“玉大人,你說我怎么就攤上這事飘诗∮氤” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵昆稿,是天一觀的道長纺座。 經(jīng)常有香客問我,道長貌嫡,這世上最難降的妖魔是什么比驻? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮岛抄,結(jié)果婚禮上别惦,老公的妹妹穿的比我還像新娘。我一直安慰自己夫椭,他們只是感情好掸掸,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蹭秋,像睡著了一般扰付。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仁讨,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天羽莺,我揣著相機(jī)與錄音,去河邊找鬼洞豁。 笑死盐固,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的丈挟。 我是一名探鬼主播刁卜,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼曙咽!你這毒婦竟也來了蛔趴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤例朱,失蹤者是張志新(化名)和其女友劉穎孝情,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洒嗤,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咧叭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了烁竭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菲茬。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖派撕,靈堂內(nèi)的尸體忽然破棺而出婉弹,到底是詐尸還是另有隱情,我是刑警寧澤终吼,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布镀赌,位于F島的核電站,受9級特大地震影響际跪,放射性物質(zhì)發(fā)生泄漏商佛。R本人自食惡果不足惜喉钢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望良姆。 院中可真熱鬧肠虽,春花似錦、人聲如沸玛追。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痊剖。三九已至韩玩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陆馁,已是汗流浹背找颓。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叮贩,地道東北人叮雳。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像妇汗,于是被迫代替她去往敵國和親帘不。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理杨箭,服務(wù)發(fā)現(xiàn)寞焙,斷路器,智...
    卡卡羅2017閱讀 134,601評論 18 139
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫互婿、插件捣郊、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,033評論 4 62
  • 1 自從微信發(fā)明了紅包這個功能驮配,越發(fā)覺得這是個好東西娘扩。 昨天又和男票吵架了,我氣壞了壮锻,后果很嚴(yán)重琐旁。但是最后給我發(fā)了...
    山那邊海對岸閱讀 412評論 0 1
  • 中國傳統(tǒng)文化是中華文明演化而匯集成的一種反映民族特質(zhì)和風(fēng)貌的民族文化掰邢,是民族歷史上各種思想文化牺陶、觀念形態(tài)的總體表征...
    高靜娟閱讀 704評論 0 0
  • 我開始好像明白我要怎么做了伟阔,卻又在某一瞬間害怕自己做不到,無論如何掰伸,這條路還是要走下去的皱炉,不斷地挫敗也好,那只是人...
    大鵬711閱讀 152評論 0 0