關注倉庫,及時得到更新:iOS-Source-Code-Analyze
Follow: Draveness · Githubgit
從開始寫 DKNightVersion 這個框架到如今已經將近一年了,目前整個框架的設計也趨於穩定。github
其實夜間模式的實現就是至關於多主題加顏色管理。而最新版本的 DKNightVersion 已經很好的解決了這個問題。編程
在正式介紹目前版本的實現以前,我會先簡單介紹一下 1.0 時代的 DKNightVersion 的實現,爲各位讀者帶來一些新的思路,也確實想梳理一下這個框架是如何演變的。數組
咱們會以對
backgroundColor
爲例說明整個框架的工做原理。安全
如何在不改變原有的架構,甚至不改變原有的代碼的基礎上,爲應用優雅地添加夜間模式成爲不少開發者不得不面對的問題。這也是 1.0 時代的 DKNightVersion 想要實現的目標。ruby
其核心思路就是使用方法調劑修改 backgroundColor
的存取方法。架構
在思考以後,我想到,想要在不改動原有代碼的基礎上實現夜間模式只能經過在分類中添加 nightBackgroundColor
屬性,而且使用方法調劑改變 backgroundColor
的 setter 方法。框架
- (void)hook_setBackgroundColor:(UIColor*)backgroundColor { if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) { [self setNormalBackgroundColor:backgroundColor]; } [self hook_setBackgroundColor:backgroundColor]; }
在當前主題爲 DKThemeVersionNormal
時,將顏色保存至 normalBackgroundColor
中,而後再調用原 backgroundColor
的 setter 方法,更新視圖的顏色。this
這裏只解決了顏色設置的問題,下面會說明,若是在主題改變時,實時更新顏色,而不用從新進入當前頁面。atom
整個 DKNightVersion 都是由一個 DKNightVersionManager
的單例來管理的,而它的主要工做就是負責改變應用的主題、並在主題改變時通知其它視圖更新顏色:
- (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]; } } } } }
若是主題更新,那麼就會遞歸地調用 changeColor
方法,刷新所有的視圖顏色,而這個方法的實現比較簡單:
- (void)changeColor { if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) { self.backgroundColor = self.normalBackgroundColor; } else { self.backgroundColor = self.nightBackgroundColor; } }
上面就是整個框架在 1.0 版本時的實現思路。不過這個版本的 DKNightVersion 在實際應用中會有比較多的問題:
在高速滾動的 scrollView
上面來回切換夜間模式,會出現顏色錯亂的問題
因爲對 backgroundColor
屬性進行不合適的方法調劑,其行爲沒法預測,好比:在設置顏色後,再取出,不必定與設置時傳入的顏色相同
沒法適配第三方 UI 控件
爲了解決 1.0 中的各類問題,我決定在 2.0 版本中放棄對 nightBackgroundColor
的使用,而且從新設計底層的實現,轉而使用更爲穩定、安全的方法實現夜間模式,先看一下效果圖:
新的實現不只可以支持夜間模式,並且可以支持多主題。
與上一個版本實現上的不一樣,在 2.0 中刪除了所有的 nightBackgroundColor
,使用一個名爲 dk_backgroundColorPicker
的屬性取代它。
@property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;
這個屬性其實就是一個 block,它接收參數 DKThemeVersion *themeVersion
,可是會返回一個 UIColor *
:
在第一次傳入 picker 或者每次主題改變時,都會將當前主題
DKThemeVersion
傳入 picker 並執行,而後,將獲得的UIColor
賦值給對應的屬性backgroundColor
更新視圖顏色。
typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);
好比下面使用 DKColorPickerWithRGB
建立一個臨時的 DKColorPicker
:
在 DKThemeVersionNormal
時返回 0xffffff
在 DKThemeVersionNight
時返回 0x343434
在自定義的主題下返回 0xfafafa
(這裏的順序與色表中主題的順序有關)
cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);
同時,每個對象還持有一個 pickers
數組,來存儲本身的所有 DKColorPicker
:
@interface NSObject () @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers; @end
在第一次使用這個屬性時,當前對象註冊爲 DKNightVersionThemeChangingNotificaiton
通知的觀察者。
在每次收到通知時,都會調用 night_update
方法,將當前主題傳入 DKColorPicker
,並再次執行,並將結果傳入對應的屬性 [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 }]; }]; }
也就是說,在每次改變主題的時候,都會發出通知。
雖然咱們在上面臨時建立了一些 DKColorPicker
。不過在 DKNightVersion
中,我更推薦使用色表,來減小相同的 DKColorPicker
的建立,而且可以更好地管理整個應用中的顏色:
NORMAL NIGHT RED #ffffff #343434 #fafafa BG #aaaaaa #313131 #aaaaaa SEP #0000ff #ffffff #fa0000 TINT #000000 #ffffff #000000 TEXT #ffffff #444444 #ffffff BAR
上面就是默認色表文件 DKColorTable.txt
中的內容,其中,第一行表示主題,NORMAL
主題必須存在,並且必須爲第一列,而最右面的 BG
、SEP
就是對應 DKColorPicker
的 key。
self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG);
在使用時,上面的代碼就至關於返回了一個在 NORMAL
時返回 #ffffff
、NIGHT
時返回 #343434
以及 RED
時返回 #fafafa
的 DKColorPicker
。
雖說,咱們使用色表以及 DKColorPicker
解決了,可是,到目前爲止咱們尚未解決第三方框架的問題。
好比咱們使用了某個第三方框架,或者本身添加了某個 color
屬性,好比說:
@interface DKView () @property (nonatomic, strong) UIColor *weirdColor; @end
weirdColor
並無對應的 DKColorPicker
,可是,咱們能夠經過 pickerify
在想要使用 dk_weirdColorPicker
的地方生成這個對應的 picker:
@pickerify(DKView, weirdColor);
而後,咱們就可使用 dk_weirdColorPicker
屬性了:
view.dk_weirdColorPicker = DKColorPickerWithKey(BG);
pickerify
實際上是一個宏:
#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
這個宏根據傳入的類和屬性名,爲咱們生成了對應 picker
的存取方法,它也能夠說是一種元編程的手段。
這裏生成的 setter 方法不是標準意義上的駝峯命名法
dk_setweirdColorPicker:
,由於我不知道怎麼才能讓大寫首字母以後的屬性添加到這裏(若是各位讀者有解決方案,歡迎提 PR 或者 issue)。
因爲框架中不少的代碼,都是重複的,因此在這裏使用了嵌入式 Ruby 模板來生成對應的文件 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
這部分的實現並不在這篇文章的討論範圍以內,若是,對這部分看興趣,能夠看一下倉庫中的 generator
文件夾,其中包含了代碼生成器的所有代碼。
若是你對 DKNightVersion 的使用有興趣,能夠查看倉庫的 README 文件,有人會說不要在項目中 ObjC runtime,我我的以爲是沒有問題,AFNetworking
、 BlocksKit
也使用方法調劑來改變原有方法的實現,不能由於它強大就不使用它;正相反,有時候,使用 runtime 才能優雅地解決問題。
關注倉庫,及時得到更新:iOS-Source-Code-Analyze
Follow: Draveness · Github