JXTheme:iOS9+換膚/暗黑模式最佳方案之一骤肛,輕量級(jí)、高度自定義窍蓝、swift編寫

簡(jiǎn)介

2018年蘋果在macOS系統(tǒng)引入了暗黑模式腋颠,一經(jīng)推出廣受好評(píng)。尤其是我們程序員吓笙,經(jīng)常與代碼淑玫、文本打交道,亮色風(fēng)格的界面看久了面睛,眼睛會(huì)特別累絮蒿。有了暗黑模式之后,我們的眼睛終于能被溫柔對(duì)待了叁鉴。而且系統(tǒng)內(nèi)置的應(yīng)用適配的非常好土涝,拿我們常用的XCode來說,也挑不出什么大毛病亲茅。反正我是用了暗黑模式之后就沒有回去過了回铛。

當(dāng)然推出暗黑模式不只是為了程序員準(zhǔn)備的,也有其他的原因:

  • 可以當(dāng)做夜間模式:晚上看屏幕的時(shí)候克锣,不會(huì)亮到你睜不開眼睛茵肃。
  • 信息重點(diǎn)的表達(dá)需要:在黑色系更能突出關(guān)鍵信息,能做到一目了然袭祟,抓住用戶的焦點(diǎn)验残。
  • 用戶審美的需要:有相當(dāng)多的用戶對(duì)黑色系的產(chǎn)品很鐘愛,當(dāng)然要迎合他們的需求了巾乳。
  • 硬件設(shè)備省電的需要:現(xiàn)在流行的OLED屏幕您没,對(duì)于純黑色像素點(diǎn)是不需要通電的。

其實(shí)不管它有多少原因胆绊,蘋果爸爸這么大力的推廣氨鹏,肯定有它的價(jià)值,我們跟著蘋果爸爸走的就行压状。這不iOS13就引入到了iOS系統(tǒng)仆抵,對(duì)于我們開發(fā)者來說,就是又愛又恨啊种冬。如果你們的產(chǎn)品是有格調(diào)的產(chǎn)品镣丑,多半暗黑模式適配的需求就在路上了,就像我一樣??娱两。但是最尷尬的地方在于莺匠,適配暗黑模式的api只在iOS13可用,你又要讓我適配暗黑模式十兢,又要讓我最低支持iOS9趣竣,你這不是讓我為難嗎??摇庙?沒辦法系統(tǒng)原生支持不了的,那就到咱們的寶藏網(wǎng)站Github上面找一找iOS9+的換膚方案期贫。當(dāng)然找到了許多跟匆,大部分是OC的,因?yàn)轫?xiàng)目主要語言是Swift通砍,所以pass掉玛臂。找到了許多swift三方庫,但是里面的一些設(shè)計(jì)有點(diǎn)過時(shí)封孙、有些不支持swift5迹冤、有些功能太重了。我只想要一個(gè)輕量級(jí)虎忌、高度自定義的方案即可泡徙。沒有現(xiàn)成的滿足的庫,與其委曲求全膜蠢,不如自己實(shí)現(xiàn)堪藐。所以就有了JXTheme方案,一個(gè)輕量級(jí)挑围、api友好礁竞、高度自定義的換膚方案。

該方案主要借鑒了iOS13的暗黑模式適配API杉辙,所以建議你先去網(wǎng)上查閱iOS13的暗黑模式適配指南模捂。先對(duì)系統(tǒng)的方案有一定了解,再來看JXTheme你會(huì)感到非常親切蜘矢。關(guān)鍵在于JXTheme最低支持iOS9狂男,等于說在iOS9就能使用iOS13的暗黑模式適配方案。而且后面還給出了當(dāng)你的應(yīng)用最低支持iOS13時(shí)品腹,從JXTheme切換到系統(tǒng)API的指南岖食。

Github地址

大家可以先進(jìn)入github地址,看一下效果舞吭。JXTheme Github地址

核心代碼&關(guān)鍵流程介紹

下面按照換膚API的調(diào)用流程來介紹實(shí)現(xiàn)方案

1.如何優(yōu)雅的設(shè)置主題屬性

通過給控件擴(kuò)展命名空間屬性theme县耽,類似于SnapKitsnpKingfisherkf镣典,這樣可以將支持主題修改的屬性,集中到theme屬性唾琼。這樣比直接給控件擴(kuò)展屬性theme_backgroundColor更加優(yōu)雅兄春。
核心代碼如下:

view.theme.backgroundColor = ThemeProvider({ (style) in
    if style == .dark {
        return .white
    }else {
        return .black
    }
})

2.如何根據(jù)傳入的style配置對(duì)應(yīng)的值

借鑒iOS13系統(tǒng)APIUIColor(dynamicProvider: <UITraitCollection) -> UIColor>)。自定義ThemeProvider結(jié)構(gòu)體锡溯,初始化器為init(_ provider: @escaping ThemePropertyProvider<T>)赶舆。傳入的參數(shù)ThemePropertyProvider是一個(gè)閉包哑姚,定義為:typealias ThemePropertyProvider<T> = (ThemeStyle) -> T。這樣就可以針對(duì)不同的控件芜茵,不同的屬性配置叙量,實(shí)現(xiàn)最大化的自定義。
核心代碼參考第一步示例代碼九串。

3.如何保存主題屬性配置閉包

對(duì)控件添加Associated object屬性providers存儲(chǔ)ThemeProvider绞佩。
核心代碼如下:

public extension ThemeWrapper where Base: UIView {
    var backgroundColor: ThemeProvider<UIColor>? {
        set(new) {
            if new != nil {
                let baseItem = self.base
                let config: ThemeCustomizationClosure = {[weak baseItem] (style) in
                    baseItem?.backgroundColor = new?.provider(style)
                }
                //存儲(chǔ)在擴(kuò)展屬性providers里面
                var newProvider = new
                newProvider?.config = config
                self.base.providers["UIView.backgroundColor"] = newProvider
                ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
            }else {
                self.base.configs.removeValue(forKey: "UIView.backgroundColor")
            }
        }
        get { return self.base.providers["UIView.backgroundColor"] as? ThemeProvider<UIColor> }
    }
}

4.如何記錄支持主題屬性的控件

為了在主題切換的時(shí)候,通知到支持主題屬性配置的控件猪钮。通過在設(shè)置主題屬性時(shí)品山,就記錄目標(biāo)控件。
核心代碼就是第3步里面的這句代碼:

ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)

然后它會(huì)被記錄到ThemeManagertrackedHashTable屬性里面烤低。因?yàn)?code>trackedHashTable是NSHashTable<AnyObject>.init(options: .weakMemory)肘交,通過弱引用記錄控件,所以不存在內(nèi)存問題扑馁。

5.如何切換主題并調(diào)用主題屬性配置閉包

通過ThemeManager.changeTheme(to: style)完成主題切換涯呻,方法內(nèi)部再調(diào)用被追蹤的控件的providers里面的ThemeProvider.provider主題屬性配置閉包。
核心代碼如下:

public func changeTheme(to style: ThemeStyle) {
    currentThemeStyle = style
    self.trackedHashTable.allObjects.forEach { (object) in
        if let view = object as? UIView {
            view.providers.values.forEach { self.resolveProvider($0) }
        }
    }
}
private func resolveProvider(_ object: Any) {
    //castdown泛型
    if let provider = object as? ThemeProvider<UIColor> {
        provider.config?(currentThemeStyle)
    }else ...
}

預(yù)覽

preview

特性

  • [x] 支持iOS 9+腻要,讓你的APP更早的實(shí)現(xiàn)DarkMode;
  • [x] 使用theme命名空間屬性:view.theme.xx = xx复罐。告別theme_xx屬性擴(kuò)展用法;
  • [x] 使用ThemeProvider傳入閉包配置闯第。根據(jù)不同的ThemeStyle完成主題屬性配置市栗,實(shí)現(xiàn)最大化的自定義;
  • [x] ThemeStyle可通過extension自定義style咳短,不再局限于lightdark;
  • [x] 提供customization屬性填帽,作為主題切換的回調(diào)入口,可以靈活配置任何屬性咙好。不再局限于提供的backgroundColor篡腌、textColor等屬性;
  • [x] 支持控件設(shè)置overrideThemeStyle勾效,會(huì)影響到其子視圖嘹悼;
  • [x] 提供根據(jù)ThemeStyle配置屬性的常規(guī)封裝、Plist文件靜態(tài)加載层宫、服務(wù)器動(dòng)態(tài)加載示例杨伙;

使用示例

擴(kuò)展ThemeStyle添加自定義style

ThemeStyle內(nèi)部?jī)H提供了一個(gè)默認(rèn)的unspecifiedstyle,其他的業(yè)務(wù)style需要自己添加萌腿,比如只支持lightdark限匣,代碼如下:

extension ThemeStyle {
    static let light = ThemeStyle(rawValue: "light")
    static let dark = ThemeStyle(rawValue: "dark")
}

基礎(chǔ)使用

view.theme.backgroundColor = ThemeProvider({ (style) in
    if style == .dark {
        return .white
    }else {
        return .black
    }
})
imageView.theme.image = ThemeProvider({ (style) in
    if style == .dark {
        return UIImage(named: "catWhite")!
    }else {
        return UIImage(named: "catBlack")!
    }
})

自定義屬性配置

view.theme.customization = ThemeProvider({[weak self] style in
    //可以選擇任一其他屬性
    if style == .dark {
        self?.view.bounds = CGRect(x: 0, y: 0, width: 30, height: 30)
    }else {
        self?.view.bounds = CGRect(x: 0, y: 0, width: 80, height: 80)
    }
})

配置封裝示例

JXTheme是一個(gè)提供主題屬性配置的輕量級(jí)基礎(chǔ)庫,不限制使用哪種方式加載資源毁菱。下面提供的三個(gè)示例僅供參考米死。

常規(guī)配置封裝示例

一般的換膚需求锌历,都會(huì)有一個(gè)UI標(biāo)準(zhǔn)。比如UILabel.textColor定義三個(gè)等級(jí)峦筒,代碼如下:

enum TextColorLevel: String {
    case normal
    case mainTitle
    case subTitle
}

然后可以封裝一個(gè)全局函數(shù)傳入TextColorLevel返回對(duì)應(yīng)的配置閉包究西,就可以極大的減少配置時(shí)的代碼量,全局函數(shù)如下:

func dynamicTextColor(_ level: TextColorLevel) -> ThemeProvider<UIColor> {
    switch level {
    case .normal:
        return ThemeProvider({ (style) in
            if style == .dark {
                return UIColor.white
            }else {
                return UIColor.gray
            }
        })
    case .mainTitle:
        ...
    case .subTitle:
        ...
    }
}

主題屬性配置時(shí)的代碼如下:

themeLabel.theme.textColor = dynamicTextColor(.mainTitle)

本地Plist文件配置示例

常規(guī)配置封裝一樣物喷,只是該方法是從本地Plist文件加載配置的具體值卤材,具體代碼參加ExampleStaticSourceManager

根據(jù)服務(wù)器動(dòng)態(tài)添加主題

常規(guī)配置封裝一樣,只是該方法是從服務(wù)器加載配置的具體值脯丝,具體代碼參加ExampleDynamicSourceManager

有狀態(tài)的控件

某些業(yè)務(wù)需求會(huì)存在一個(gè)控件有多種狀態(tài)商膊,比如選中與未選中。不同的狀態(tài)對(duì)于不同的主題又會(huì)有不同的配置宠进。配置代碼參考如下:

statusLabel.theme.textColor = ThemeProvider({[weak self] (style) in
    if self?.statusLabelStatus == .isSelected {
        //選中狀態(tài)一種配置
        if style == .dark {
            return .red
        }else {
            return .green
        }
    }else {
        //未選中狀態(tài)另一種配置
        if style == .dark {
            return .white
        }else {
            return .black
        }
    }
})

當(dāng)控件的狀態(tài)更新時(shí)晕拆,需要刷新當(dāng)前的主題屬性配置,代碼如下:

func statusDidChange() {
    statusLabel.theme.textColor?.refresh()
}

如果你的控件支持多個(gè)狀態(tài)屬性材蹬,比如有textColor实幕、backgroundColorfont等等堤器,你可以不用一個(gè)一個(gè)的主題屬性調(diào)用refresh方法昆庇,可以使用下面的代碼完成所有配置的主題屬性刷新:

func statusDidChange() {
    statusLabel.theme.refresh()
}

overrideThemeStyle

不管主題如何切換,overrideThemeStyleParentView及其子視圖的themeStyle都是dark

overrideThemeStyleParentView.theme.overrideThemeStyle = .dark

其他說明

為什么使用theme命名空間屬性闸溃,而不是使用theme_xx擴(kuò)展屬性呢整吆?

  • 如果你給系統(tǒng)的類擴(kuò)展了N個(gè)函數(shù),當(dāng)你在使用該類時(shí)辉川,進(jìn)行函數(shù)索引時(shí)表蝙,就會(huì)有N個(gè)擴(kuò)展的方法干擾你的選擇。尤其是你在進(jìn)行其他業(yè)務(wù)開發(fā)乓旗,而不是想配置主題屬性時(shí)府蛇。
  • KingfisherSnapKit等知名三方庫屿愚,都使用了命名空間屬性實(shí)現(xiàn)對(duì)系統(tǒng)類的擴(kuò)展汇跨,這是一個(gè)更Swift的寫法,值得學(xué)習(xí)妆距。

主題切換通知

extension Notification.Name {
    public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
}

ThemeManager根據(jù)用戶ID存儲(chǔ)主題配置

/// 配置存儲(chǔ)的標(biāo)志key穷遂。可以設(shè)置為用戶的ID娱据,這樣在同一個(gè)手機(jī)蚪黑,可以分別記錄不同用戶的配置。需要優(yōu)先設(shè)置該屬性再設(shè)置其他值。
public var storeConfigsIdentifierKey: String = "default"

遷移到系統(tǒng)API指南

當(dāng)你的應(yīng)用最低支持iOS13時(shí)祠锣,如果需要的話可以按照如下指南,遷移到系統(tǒng)方案咽安。
遷移到系統(tǒng)API指南伴网,點(diǎn)擊閱讀

Github地址

最后再復(fù)習(xí)一下github地址,點(diǎn)擊進(jìn)入查看更多細(xì)節(jié)妆棒。JXTheme Github地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末澡腾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子糕珊,更是在濱河造成了極大的恐慌动分,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件红选,死亡現(xiàn)場(chǎng)離奇詭異澜公,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)喇肋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門坟乾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蝶防,你說我怎么就攤上這事甚侣。” “怎么了间学?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵殷费,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我低葫,道長(zhǎng)详羡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任氮采,我火速辦了婚禮殷绍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鹊漠。我一直安慰自己主到,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布躯概。 她就那樣靜靜地躺著登钥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪娶靡。 梳的紋絲不亂的頭發(fā)上牧牢,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼塔鳍。 笑死伯铣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的轮纫。 我是一名探鬼主播腔寡,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼掌唾!你這毒婦竟也來了放前?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤糯彬,失蹤者是張志新(化名)和其女友劉穎凭语,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撩扒,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡似扔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了却舀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虫几。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖挽拔,靈堂內(nèi)的尸體忽然破棺而出辆脸,到底是詐尸還是另有隱情,我是刑警寧澤螃诅,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布啡氢,位于F島的核電站,受9級(jí)特大地震影響术裸,放射性物質(zhì)發(fā)生泄漏倘是。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一袭艺、第九天 我趴在偏房一處隱蔽的房頂上張望搀崭。 院中可真熱鬧,春花似錦猾编、人聲如沸瘤睹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轰传。三九已至,卻和暖如春瘪撇,著一層夾襖步出監(jiān)牢的瞬間获茬,已是汗流浹背港庄。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恕曲,地道東北人鹏氧。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像佩谣,于是被迫代替她去往敵國(guó)和親度帮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程稿存,因...
    小菜c閱讀 6,444評(píng)論 0 17
  • 我們?cè)谑褂帽镜厮接兴饕龓斓臅r(shí)候還存在一些問題,我們需要做一些優(yōu)化 1.需要我們手動(dòng)創(chuàng)建spec文件 2.需要手動(dòng)使...
    MagicCare閱讀 250評(píng)論 0 1
  • 開篇前瞳秽,大家先看看下面問題有沒有發(fā)生在自己身上瓣履? 上班時(shí)間內(nèi)總是完不成工作。 工作總是不順练俐,很難往下開展袖迎。 文件和...
    整理師睿姿閱讀 428評(píng)論 0 1
  • 文/王漫 導(dǎo)語: 生容易,活容易腺晾,生活不容易!人生是一本書燕锥,等你來解讀。生活不光是柴米油鹽醬醋茶悯蝉,更應(yīng)擁有琴棋書畫...
    武商路漫漫閱讀 1,060評(píng)論 17 29