成熟的夜間模式解決方案(iOS)

原文鏈接: 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ì)有比較多的問題:

  1. 在高速滾動(dòng)的 scrollView 上面來回切換夜間模式,會(huì)出現(xiàn)顏色錯(cuò)亂的問題
  2. 由于對(duì) backgroundColor 屬性進(jìn)行不合適的方法調(diào)劑梁剔,其行為無(wú)法預(yù)測(cè)虽画,比如:在設(shè)置顏色后,再取出荣病,不一定與設(shè)置時(shí)傳入的顏色相同
  3. 無(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

  1. DKThemeVersionNormal 時(shí)返回 0xffffff
  2. DKThemeVersionNight 時(shí)返回 0x343434
  3. 在自定義的主題下返回 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 主題必須存在驻仅,而且必須為第一列谅畅,而最右面的 BGSEP 就是對(duì)應(yīng) DKColorPicker 的 key噪服。

self.tableView.dk_backgroundColorPicker =  DKColorPickerWithKey(BG);

在使用時(shí)毡泻,上面的代碼就相當(dāng)于返回了一個(gè)在 NORMAL 時(shí)返回 #ffffffNIGHT 時(shí)返回 #343434 以及 RED 時(shí)返回 #fafafaDKColorPicker粘优。

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è)人覺得是沒有問題盾鳞,AFNetworkingBlocksKit 也使用方法調(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肉迫,一起剝皮案震驚了整個(gè)濱河市验辞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昂拂,老刑警劉巖受神,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異格侯,居然都是意外死亡鼻听,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門联四,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撑碴,“玉大人,你說我怎么就攤上這事朝墩∽硗兀” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)亿卤。 經(jīng)常有香客問我愤兵,道長(zhǎng),這世上最難降的妖魔是什么排吴? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任秆乳,我火速辦了婚禮,結(jié)果婚禮上钻哩,老公的妹妹穿的比我還像新娘屹堰。我一直安慰自己,他們只是感情好街氢,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布扯键。 她就那樣靜靜地躺著,像睡著了一般珊肃。 火紅的嫁衣襯著肌膚如雪荣刑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天近范,我揣著相機(jī)與錄音嘶摊,去河邊找鬼。 笑死评矩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的阱飘。 我是一名探鬼主播斥杜,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼沥匈!你這毒婦竟也來了蔗喂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤高帖,失蹤者是張志新(化名)和其女友劉穎缰儿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體散址,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乖阵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了预麸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞪浸。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吏祸,靈堂內(nèi)的尸體忽然破棺而出对蒲,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布蹈矮,位于F島的核電站砰逻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏泛鸟。R本人自食惡果不足惜诱渤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谈况。 院中可真熱鬧勺美,春花似錦、人聲如沸碑韵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)祝闻。三九已至占卧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間联喘,已是汗流浹背华蜒。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豁遭,地道東北人叭喜。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蓖谢,于是被迫代替她去往敵國(guó)和親捂蕴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

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

  • 成熟的夜間模式 從開始寫DKNightVersion這個(gè)框架到現(xiàn)在已經(jīng)將近一年了闪幽,目前整個(gè)框架的設(shè)計(jì)也趨于穩(wěn)定啥辨。 ...
    從小荔枝炸清華閱讀 447評(píng)論 0 1
  • 昨晚,手機(jī)連續(xù)傳出微信信息推送的聲音盯腌,我內(nèi)心納悶溉知,這又不是周末啊。 然后打開微信之后腕够,與自己上班密切相關(guān)的消息傳了...
    女公子99閱讀 266評(píng)論 0 4
  • 拖了這么久才把《雪人》徹底啃完级乍。作為挪威推理小說暢銷冠軍以及獲得愛倫坡獎(jiǎng)提名的作品,這部小說絕對(duì)擔(dān)得起任何溢美之詞...
    一蓑煙雨_6c60閱讀 225評(píng)論 0 0
  • 《相親十年》--海格 今天閱讀了這樣一本書燕少,心下感到非常慶幸卡者,因?yàn)樵鞠胍业臅鴽]有找到,隨手拿起了附近的這本...
    心理咨詢師李彬閱讀 482評(píng)論 0 1