簡(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
县耽,類似于SnapKit
的snp
、Kingfisher
的kf
镣典,這樣可以將支持主題修改的屬性,集中到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ì)被記錄到ThemeManager
的trackedHashTable
屬性里面烤低。因?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ù)覽
特性
- [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咳短,不再局限于light
和dark
; - [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)的unspecified
style,其他的業(yè)務(wù)style需要自己添加萌腿,比如只支持light
和dark
限匣,代碼如下:
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文件加載配置的具體值卤材,具體代碼參加Example
的StaticSourceManager
類
根據(jù)服務(wù)器動(dòng)態(tài)添加主題
與常規(guī)配置封裝一樣,只是該方法是從服務(wù)器加載配置的具體值脯丝,具體代碼參加Example
的DynamicSourceManager
類
有狀態(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
实幕、backgroundColor
、font
等等堤器,你可以不用一個(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í)府蛇。
- 像
Kingfisher
、SnapKit
等知名三方庫屿愚,都使用了命名空間屬性實(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地址