原文鏈接: http://draveness.me/night/
關(guān)注倉(cāng)庫(kù),及時(shí)獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github
從開始寫 DKNightVersion 這個(gè)框架到現(xiàn)在已經(jīng)將近一年了裕菠,目前整個(gè)框架的設(shè)計(jì)也趨于穩(wěn)定牵舱。
其實(shí)夜間模式的實(shí)現(xiàn)就是相當(dāng)于多主題加顏色管理隆檀。而最新版本的 DKNightVersion 已經(jīng)很好的解決了這個(gè)問題。
在正式介紹目前版本的實(shí)現(xiàn)之前,我會(huì)先簡(jiǎn)單介紹一下 1.0 時(shí)代的 DKNightVersion 的實(shí)現(xiàn)到涂,為各位讀者帶來一些新的思路懈糯,也確實(shí)想梳理一下這個(gè)框架是如何演變的涤妒。
我們會(huì)以對(duì)
backgroundColor
為例說明整個(gè)框架的工作原理。
方法調(diào)劑的版本
如何在不改變?cè)械募軜?gòu)赚哗,甚至不改變?cè)械拇a的基礎(chǔ)上她紫,為應(yīng)用優(yōu)雅地添加夜間模式成為很多開發(fā)者不得不面對(duì)的問題硅堆。這也是 1.0 時(shí)代的 DKNightVersion 想要實(shí)現(xiàn)的目標(biāo)。
其核心思路就是使用方法調(diào)劑修改 backgroundColor
的存取方法贿讹。
使用 nightBackgroundColor
在思考之后渐逃,我想到,想要在不改動(dòng)原有代碼的基礎(chǔ)上實(shí)現(xiàn)夜間模式只能通過在分類中添加 nightBackgroundColor
屬性民褂,并且使用方法調(diào)劑改變 backgroundColor
的 setter 方法茄菊。
- (void)hook_setBackgroundColor:(UIColor*)backgroundColor {
if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
[self setNormalBackgroundColor:backgroundColor];
}
[self hook_setBackgroundColor:backgroundColor];
}
在當(dāng)前主題為 DKThemeVersionNormal
時(shí),將顏色保存至 normalBackgroundColor
中赊堪,然后再調(diào)用原 backgroundColor
的 setter 方法面殖,更新視圖的顏色。
DKNightVersionManager
這里只解決了顏色設(shè)置的問題哭廉,下面會(huì)說明脊僚,如果在主題改變時(shí),實(shí)時(shí)更新顏色遵绰,而不用重新進(jìn)入當(dāng)前頁(yè)面辽幌。
整個(gè) DKNightVersion 都是由一個(gè) DKNightVersionManager
的單例來管理的,而它的主要工作就是負(fù)責(zé)改變應(yīng)用的主題椿访、并在主題改變時(shí)通知其它視圖更新顏色:
- (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {
if ([object respondsToSelector:@selector(changeColor)]) {
[object changeColor];
}
if ([object respondsToSelector:@selector(subviews)]) {
if (![object subviews]) {
// Basic case, do nothing.
return;
} else {
for (id subview in [object subviews]) {
// recursive darken all the subviews of current view.
[self changeColor: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)思路逛犹。不過這個(gè)版本的 DKNightVersion 在實(shí)際應(yīng)用中會(huì)有比較多的問題:
- 在高速滾動(dòng)的
scrollView
上面來回切換夜間模式,會(huì)出現(xiàn)顏色錯(cuò)亂的問題 - 由于對(duì)
backgroundColor
屬性進(jìn)行不合適的方法調(diào)劑梁剔,其行為無(wú)法預(yù)測(cè)虽画,比如:在設(shè)置顏色后,再取出荣病,不一定與設(shè)置時(shí)傳入的顏色相同 - 無(wú)法適配第三方 UI 控件
使用色表的版本
為了解決 1.0 中的各種問題码撰,我決定在 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
更新視圖顏色溜宽。
typedef UIColor *(^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ù)組,來存儲(chǔ)自己的全部 DKColorPicker
:
@interface NSObject ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *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:^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:sel withObject:result];
#pragma clang diagnostic pop
}];
}];
}
也就是說,在每次改變主題的時(shí)候什猖,都會(huì)發(fā)出通知票彪。
DKColorTable
雖然我們?cè)谏厦媾R時(shí)創(chuàng)建了一些 DKColorPicker
。不過在 DKNightVersion
中不狮,我更推薦使用色表降铸,來減少相同的 DKColorPicker
的創(chuàng)建,并且能夠更好地管理整個(gè)應(yīng)用中的顏色:
NORMAL NIGHT RED
#ffffff #343434 #fafafa BG
#aaaaaa #313131 #aaaaaa SEP
#0000ff #ffffff #fa0000 TINT
#000000 #ffffff #000000 TEXT
#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
雖然說仇味,我們使用色表以及 DKColorPicker
解決了,但是雹顺,到目前為止我們還沒有解決第三方框架的問題丹墨。
比如我們使用了某個(gè)第三方框架,或者自己添加了某個(gè) color
屬性嬉愧,比如說:
@interface DKView ()
@property (nonatomic, strong) UIColor *weirdColor;
@end
weirdColor
并沒有對(duì)應(yīng)的 DKColorPicker
贩挣,但是,我們可以通過 pickerify
在想要使用 dk_weirdColorPicker
的地方生成這個(gè)對(duì)應(yīng)的 picker:
@pickerify(DKView, weirdColor);
然后,我們就可以使用 dk_weirdColorPicker
屬性了:
view.dk_weirdColorPicker = DKColorPickerWithKey(BG);
pickerify
其實(shí)是一個(gè)宏:
#define pickerify(KLASS, PROPERTY) interface \\
KLASS (Night) \\
@property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \\
@end \\
@interface \\
KLASS () \\
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *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ù)傳入的類和屬性名揽惹,為我們生成了對(duì)應(yīng) picker
的存取方法被饿,它也可以說是一種元編程的手段。
這里生成的 setter 方法不是標(biāo)準(zhǔn)意義上的駝峰命名法
dk_setweirdColorPicker:
搪搏,因?yàn)槲也恢涝趺床拍茏尨髮懯鬃帜钢蟮膶傩蕴砑拥竭@里(如果各位讀者有解決方案狭握,歡迎提 PR 或者 issue)。
嵌入式 Ruby
由于框架中很多的代碼疯溺,都是重復(fù)的论颅,所以在這里使用了嵌入式 Ruby 模板來生成對(duì)應(yīng)的文件 color.m.irb
:
//
// <%= klass.name %>+Night.m
// <%= klass.name %>+Night
//
// Copyright (c) 2015 Draveness. All rights reserved.
//
// These files are generated by ruby script, if you want to modify code
// in this file, you are supposed to update the ruby code, run it and
// test it. And finally open a pull request.
#import "<%= klass.name %>+Night.h"
#import "DKNightVersionManager.h"
#import <objc/runtime.h>
@interface <%= klass.name %> ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
@end
@implementation <%= klass.name %> (Night)
<% klass.properties.each do |property| %><%= """
- (DKColorPicker)dk_#{property.name}Picker {
return objc_getAssociatedObject(self, @selector(dk_#{property.name}Picker));
}
- (void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker {
objc_setAssociatedObject(self, @selector(dk_#{property.name}Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
self.#{property.name} = picker(self.dk_manager.themeVersion);
[self.pickers setValue:[picker copy] forKey:@\\"#{property.setter}\\"];
}
""" %><% end %>
@end
這部分的實(shí)現(xiàn)并不在這篇文章的討論范圍之內(nèi),如果囱嫩,對(duì)這部分看興趣恃疯,可以看一下倉(cāng)庫(kù)中的 generator
文件夾,其中包含了代碼生成器的全部代碼墨闲。
小結(jié)
如果你對(duì) DKNightVersion 的使用有興趣今妄,可以查看倉(cāng)庫(kù)的 README 文件,有人會(huì)說不要在項(xiàng)目中 ObjC runtime鸳碧,我個(gè)人覺得是沒有問題盾鳞,AFNetworking
、 BlocksKit
也使用方法調(diào)劑來改變?cè)蟹椒ǖ膶?shí)現(xiàn)瞻离,不能因?yàn)樗鼜?qiáng)大就不使用它腾仅;正相反,有時(shí)候套利,使用 runtime 才能優(yōu)雅地解決問題推励。
關(guān)注倉(cāng)庫(kù),及時(shí)獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github