關(guān)注倉(cāng)庫(kù)株灸,及時(shí)獲得更新:iOS-Source-Code-Analyze
從開(kāi)始寫(xiě)DKNightVersion這個(gè)框架到現(xiàn)在已經(jīng)將近一年了堤结,目前整個(gè)框架的設(shè)計(jì)也趨于穩(wěn)定芦劣。
其實(shí)夜間模式的實(shí)現(xiàn)就是相當(dāng)于多主題加顏色管理鸽捻。而最新版本的DKNightVersion已經(jīng)很好的解決了這個(gè)問(wèn)題旧烧。
在正式介紹目前版本的實(shí)現(xiàn)之前特咆,我會(huì)先簡(jiǎn)單介紹一下 1.0 時(shí)代的 DKNightVersion 的實(shí)現(xiàn)季惩,為各位讀者帶來(lái)一些新的思路,也確實(shí)想梳理一下這個(gè)框架是如何演變的腻格。
我們會(huì)以對(duì)backgroundColor為例說(shuō)明整個(gè)框架的工作原理画拾。

方法調(diào)劑的版本
如何在不改變?cè)械募軜?gòu),甚至不改變?cè)械拇a的基礎(chǔ)上菜职,為應(yīng)用優(yōu)雅地添加夜間模式成為很多開(kāi)發(fā)者不得不面對(duì)的問(wèn)題青抛。這也是 1.0 時(shí)代的 DKNightVersion 想要實(shí)現(xiàn)的目標(biāo)。
其核心思路就是使用方法調(diào)劑修改backgroundColor的存取方法酬核。
使用 nightBackgroundColor
在思考之后蜜另,我想到,想要在不改動(dòng)原有代碼的基礎(chǔ)上實(shí)現(xiàn)夜間模式只能通過(guò)在分類(lèi)中添加nightBackgroundColor屬性嫡意,并且使用方法調(diào)劑改變backgroundColor的 setter 方法举瑰。
-(void)hook_setBackgroundColor:(UIColor*)backgroundColor{if([DKNightVersionManager currentThemeVersion]==DKThemeVersionNormal){[selfsetNormalBackgroundColor:backgroundColor];}[selfhook_setBackgroundColor:backgroundColor];}
在當(dāng)前主題為DKThemeVersionNormal時(shí),將顏色保存至normalBackgroundColor中蔬螟,然后再調(diào)用原backgroundColor的 setter 方法此迅,更新視圖的顏色。
DKNightVersionManager
這里只解決了顏色設(shè)置的問(wèn)題旧巾,下面會(huì)說(shuō)明耸序,如果在主題改變時(shí),實(shí)時(shí)更新顏色鲁猩,而不用重新進(jìn)入當(dāng)前頁(yè)面坎怪。
整個(gè) DKNightVersion 都是由一個(gè)DKNightVersionManager的單例來(lái)管理的,而它的主要工作就是負(fù)責(zé)改變應(yīng)用的主題廓握、并在主題改變時(shí)通知其它視圖更新顏色:
-(void)changeColor:(id)object{if([object respondsToSelector:@selector(changeColor)]){[object changeColor];}if([object respondsToSelector:@selector(subviews)]){if(![object subviews]){// Basic case, do nothing.return;}else{for(id subviewin[object subviews]){// recursive darken all the subviews of current view.[selfchangeColor:subview];if([subview respondsToSelector:@selector(changeColor)]){[subview changeColor];}}}}}
如果主題更新芋忿,那么就會(huì)遞歸地調(diào)用changeColor方法,刷新全部的視圖顏色疾棵,而這個(gè)方法的實(shí)現(xiàn)比較簡(jiǎn)單:
-(void)changeColor{if([DKNightVersionManager currentThemeVersion]==DKThemeVersionNormal){self.backgroundColor=self.normalBackgroundColor;}else{self.backgroundColor=self.nightBackgroundColor;}}
上面就是整個(gè)框架在 1.0 版本時(shí)的實(shí)現(xiàn)思路戈钢。不過(guò)這個(gè)版本的 DKNightVersion 在實(shí)際應(yīng)用中會(huì)有比較多的問(wèn)題:
在高速滾動(dòng)的scrollView上面來(lái)回切換夜間模式,會(huì)出現(xiàn)顏色錯(cuò)亂的問(wèn)題
由于對(duì)backgroundColor屬性進(jìn)行不合適的方法調(diào)劑是尔,其行為無(wú)法預(yù)測(cè)殉了,比如:在設(shè)置顏色后,再取出拟枚,不一定與設(shè)置時(shí)傳入的顏色相同
無(wú)法適配第三方 UI 控件
使用色表的版本
為了解決 1.0 中的各種問(wèn)題薪铜,我決定在 2.0 版本中放棄對(duì)nightBackgroundColor的使用众弓,并且重新設(shè)計(jì)底層的實(shí)現(xiàn),轉(zhuǎn)而使用更為穩(wěn)定隔箍、安全的方法實(shí)現(xiàn)夜間模式谓娃,先看一下效果圖:

新的實(shí)現(xiàn)不僅能夠支持夜間模式,而且能夠支持多主題蜒滩。
DKColorPicker
與上一個(gè)版本實(shí)現(xiàn)上的不同滨达,在 2.0 中刪除了全部的nightBackgroundColor,使用一個(gè)名為dk_backgroundColorPicker的屬性取代它俯艰。
@property(nonatomic,copy)DKColorPicker dk_backgroundColorPicker;
這個(gè)屬性其實(shí)就是一個(gè) block捡遍,它接收參數(shù)DKThemeVersion *themeVersion,但是會(huì)返回一個(gè)UIColor *:
在第一次傳入 picker 或者每次主題改變時(shí)竹握,都會(huì)將當(dāng)前主題DKThemeVersion傳入 picker 并執(zhí)行画株,然后,將得到的UIColor賦值給對(duì)應(yīng)的屬性backgroundColor更新視圖顏色啦辐。
typedefUIColor*(^DKColorPicker)(DKThemeVersion*themeVersion);
比如下面使用DKColorPickerWithRGB創(chuàng)建一個(gè)臨時(shí)的DKColorPicker:
在DKThemeVersionNormal時(shí)返回0xffffff
在DKThemeVersionNight時(shí)返回0x343434
在自定義的主題下返回0xfafafa(這里的順序與色表中主題的順序有關(guān))
cell.dk_backgroundColorPicker=DKColorPickerWithRGB(0xffffff,0x343434,0xfafafa);
同時(shí)谓传,每一個(gè)對(duì)象還持有一個(gè)pickers數(shù)組,來(lái)存儲(chǔ)自己的全部DKColorPicker:
@interfaceNSObject()@property(nonatomic,strong)NSMutableDictionary*pickers;@end
在第一次使用這個(gè)屬性時(shí)芹关,當(dāng)前對(duì)象注冊(cè)為DKNightVersionThemeChangingNotificaiton通知的觀察者良拼。
在每次收到通知時(shí),都會(huì)調(diào)用night_update方法充边,將當(dāng)前主題傳入DKColorPicker庸推,并再次執(zhí)行,并將結(jié)果傳入對(duì)應(yīng)的屬性[self performSelector:sel withObject:result]浇冰。
-(void)night_updateColor{[self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString*_Nonnull selector,DKColorPicker? _Nonnull picker,BOOL*_Nonnull stop){SEL sel=NSSelectorFromString(selector);id result=picker(self.dk_manager.themeVersion);[UIView animateWithDuration:DKNightVersionAnimationDuration? ? ? ? ? ? ? ? ? ? ? ? animations:^{#pragmaclang diagnostic push#pragmaclang diagnostic ignored "-Warc-performSelector-leaks"[selfperformSelector:sel withObject:result];#pragmaclang diagnostic pop}];}];}
也就是說(shuō)贬媒,在每次改變主題的時(shí)候,都會(huì)發(fā)出通知肘习。
DKColorTable
雖然我們?cè)谏厦媾R時(shí)創(chuàng)建了一些DKColorPicker际乘。不過(guò)在DKNightVersion中,我更推薦使用色表漂佩,來(lái)減少相同的DKColorPicker的創(chuàng)建脖含,并且能夠更好地管理整個(gè)應(yīng)用中的顏色:
NORMAL? NIGHT? ? RED#ffffff? #343434? #fafafa BG#aaaaaa? #313131? #aaaaaa SEP#0000ff#ffffff? #fa0000 TINT#000000#ffffff? #000000TEXT#ffffff? #444444? #ffffff BAR
上面就是默認(rèn)色表文件DKColorTable.txt中的內(nèi)容,其中投蝉,第一行表示主題养葵,NORMAL主題必須存在,而且必須為第一列瘩缆,而最右面的BG关拒、SEP就是對(duì)應(yīng)DKColorPicker的 key。
self.tableView.dk_backgroundColorPicker=DKColorPickerWithKey(BG);
在使用時(shí),上面的代碼就相當(dāng)于返回了一個(gè)在NORMAL時(shí)返回#ffffff着绊、NIGHT時(shí)返回#343434以及RED時(shí)返回#fafafa的DKColorPicker谐算。
pickerify
雖然說(shuō),我們使用色表以及DKColorPicker解決了归露,但是洲脂,到目前為止我們還沒(méi)有解決第三方框架的問(wèn)題。
比如我們使用了某個(gè)第三方框架剧包,或者自己添加了某個(gè)color屬性恐锦,比如說(shuō):
@interfaceDKView()@property(nonatomic,strong)UIColor*weirdColor;@end
weirdColor并沒(méi)有對(duì)應(yīng)的DKColorPicker,但是玄捕,我們可以通過(guò)pickerify在想要使用dk_weirdColorPicker的地方生成這個(gè)對(duì)應(yīng)的 picker:
@pickerify(DKView,weirdColor);
然后,我們就可以使用dk_weirdColorPicker屬性了:
view.dk_weirdColorPicker=DKColorPickerWithKey(BG);
pickerify其實(shí)是一個(gè)宏:
#definepickerify(KLASS, PROPERTY) interface \? ? KLASS (Night) \? ? @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \? ? @end \? ? @interface \? ? KLASS () \? ? @property (nonatomic, strong) NSMutableDictionary *pickers; \? ? @end \? ? @implementation \? ? KLASS (Night) \? ? - (DKColorPicker)dk_ ## PROPERTY ## Picker { \? ? ? ? return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \? ? } \? ? - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \? ? ? ? objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \? ? ? ? [self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\? ? ? ? [self.pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \? ? } \? ? @end
這個(gè)宏根據(jù)傳入的類(lèi)和屬性名棚放,為我們生成了對(duì)應(yīng)picker的存取方法枚粘,它也可以說(shuō)是一種元編程的手段。
這里生成的 setter 方法不是標(biāo)準(zhǔn)意義上的駝峰命名法dk_setweirdColorPicker:飘蚯,因?yàn)槲也恢涝趺床拍茏尨髮?xiě)首字母之后的屬性添加到這里(如果各位讀者有解決方案馍迄,歡迎提 PR 或者 issue)。