蘋果爸爸總是讓人又愛(ài)又恨啊,今年的暗黑模式注定要讓iOS開發(fā)者折騰半天。但是也再次體現(xiàn)了iOS開發(fā)者的價(jià)值,iOS生態(tài)獨(dú)特的特性和其不斷的變化與進(jìn)步,才讓iOS開發(fā)者始終被人銘記,不會(huì)完全被大前端和多端統(tǒng)一技術(shù)給淹沒(méi)。從這個(gè)角度來(lái)說(shuō),要感謝蘋果爸爸??
說(shuō)回正題,iOS13的dark mode相關(guān)API只能在iOS13以后才能使用。但是大部分的項(xiàng)目都還是會(huì)堅(jiān)持支持老系統(tǒng),以獲取更多的用戶?,F(xiàn)在網(wǎng)上有許多關(guān)于iOS13 dark mode的適配文章,相關(guān)的技術(shù)點(diǎn)都很簡(jiǎn)單。主要的是字體顏色、圖片的適配。看過(guò)之后,內(nèi)心更加悲涼,iOS13 dark mode適配我都會(huì)了,老系統(tǒng)腫么辦呢???
你需要一個(gè)輕量級(jí)、api友好、高度自定義且最低支持iOS9+的換膚方案。別擔(dān)心!我的戰(zhàn)友?? ,讓我為你推薦JXTheme方案,它主要借鑒了iOS13的暗黑模式適配API,使用JXTheme你會(huì)感到非常親切。而且當(dāng)你的應(yīng)用最低支持iOS13時(shí),可以方便的從JXTheme切換到系統(tǒng)API。
Github地址
大家可以先進(jìn)入github地址,看一下效果。JXTheme Github地址
讓我們從整個(gè)暗黑模式適配的流程來(lái)熟悉JXTheme的原理:
1.如何優(yōu)雅的設(shè)置主題屬性
通過(guò)給控件擴(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
}
})
復(fù)制代碼
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> }
}
}
復(fù)制代碼
4.如何記錄支持主題屬性的控件
為了在主題切換的時(shí)候,通知到支持主題屬性配置的控件。通過(guò)在設(shè)置主題屬性時(shí),就記錄目標(biāo)控件。 核心代碼就是第3步里面的這句代碼:
ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
復(fù)制代碼
然后它會(huì)被記錄到ThemeManager的trackedHashTable屬性里面。因?yàn)?code>trackedHashTable是NSHashTable<AnyObject>.init(options: .weakMemory),通過(guò)弱引用記錄控件,所以不存在內(nèi)存問(wèn)題。
5.如何切換主題并調(diào)用主題屬性配置閉包
通過(guò)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 ...
}
復(fù)制代碼
預(yù)覽
<figcaption></figcaption>
特性
- [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可通過(guò)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)的unspecifiedstyle,其他的業(yè)務(wù)style需要自己添加,比如只支持light和dark,代碼如下:
extension ThemeStyle {
static let light = ThemeStyle(rawValue: "light")
static let dark = ThemeStyle(rawValue: "dark")
}
復(fù)制代碼
基礎(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")!
}
})
復(fù)制代碼
自定義屬性配置
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)
}
})
復(fù)制代碼
配置封裝示例
JXTheme是一個(gè)提供主題屬性配置的輕量級(jí)基礎(chǔ)庫(kù),不限制使用哪種方式加載資源。下面提供的三個(gè)示例僅供參考。
常規(guī)配置封裝示例
一般的換膚需求,都會(huì)有一個(gè)UI標(biāo)準(zhǔn)。比如UILabel.textColor定義三個(gè)等級(jí),代碼如下:
enum TextColorLevel: String {
case normal
case mainTitle
case subTitle
}
復(fù)制代碼
然后可以封裝一個(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:
...
}
}
復(fù)制代碼
主題屬性配置時(shí)的代碼如下:
themeLabel.theme.textColor = dynamicTextColor(.mainTitle)
復(fù)制代碼
本地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
}
}
})
復(fù)制代碼
當(dāng)控件的狀態(tài)更新時(shí),需要刷新當(dāng)前的主題屬性配置,代碼如下:
func statusDidChange() {
statusLabel.theme.textColor?.refresh()
}
復(fù)制代碼
如果你的控件支持多個(gè)狀態(tài)屬性,比如有textColor、backgroundColor、font等等,你可以不用一個(gè)一個(gè)的主題屬性調(diào)用refresh方法,可以使用下面的代碼完成所有配置的主題屬性刷新:
func statusDidChange() {
statusLabel.theme.refresh()
}
復(fù)制代碼
overrideThemeStyle
不管主題如何切換,overrideThemeStyleParentView及其子視圖的themeStyle都是dark
overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
復(fù)制代碼
其他說(shuō)明
為什么使用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等知名三方庫(kù),都使用了命名空間屬性實(shí)現(xiàn)對(duì)系統(tǒng)類的擴(kuò)展,這是一個(gè)更Swift的寫法,值得學(xué)習(xí)。
主題切換通知
extension Notification.Name {
public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
}
復(fù)制代碼
ThemeManager根據(jù)用戶ID存儲(chǔ)主題配置
/// 配置存儲(chǔ)的標(biāo)志key??梢栽O(shè)置為用戶的ID,這樣在同一個(gè)手機(jī),可以分別記錄不同用戶的配置。需要優(yōu)先設(shè)置該屬性再設(shè)置其他值。
public var storeConfigsIdentifierKey: String = "default"
復(fù)制代碼
遷移到系統(tǒng)API指南
當(dāng)你的應(yīng)用最低支持iOS13時(shí),如果需要的話可以按照如下指南,遷移到系統(tǒng)方案。 遷移到系統(tǒng)API指南,點(diǎn)擊閱讀
Github地址
最后再?gòu)?fù)習(xí)一下github地址,點(diǎn)擊進(jìn)入查看更多細(xì)節(jié)。JXTheme Github地址
作者:金字塔程序員
鏈接:https://juejin.im/post/5d882ff85188253ec722e21f