蘋果爸爸老是讓人又愛又恨啊,今年的暗黑模式註定要讓iOS開發者折騰半天。可是也再次體現了iOS開發者的價值,iOS生態獨特的特性和其不斷的變化與進步,才讓iOS開發者始終被人銘記,不會徹底被大前端和多端統一技術給淹沒。從這個角度來講,要感謝蘋果爸爸😘前端
說回正題,iOS13的dark mode相關API只能在iOS13之後才能使用。可是大部分的項目都仍是會堅持支持老系統,以獲取更多的用戶。如今網上有許多關於iOS13 dark mode的適配文章,相關的技術點都很簡單。主要的是字體顏色、圖片的適配。看過以後,心裏更加悲涼,iOS13 dark mode適配我都會了,老系統腫麼辦呢😂?git
你須要一個輕量級、api友好、高度自定義且最低支持iOS9+的換膚方案。別擔憂!個人戰友👬 ,讓我爲你推薦JXTheme方案,它主要借鑑了iOS13的暗黑模式適配API,使用JXTheme你會感到很是親切。並且當你的應用最低支持iOS13時,能夠方便的從JXTheme切換到系統API。github
你們能夠先進入github地址,看一下效果。JXTheme Github地址api
經過給控件擴展命名空間屬性theme
,相似於SnapKit
的snp
、Kingfisher
的kf
,這樣能夠將支持主題修改的屬性,集中到theme
屬性。這樣比直接給控件擴展屬性theme_backgroundColor
更加優雅。 核心代碼以下:bash
view.theme.backgroundColor = ThemeProvider({ (style) in
if style == .dark {
return .white
}else {
return .black
}
})
複製代碼
借鑑iOS13系統APIUIColor(dynamicProvider: <UITraitCollection) -> UIColor>)
。自定義ThemeProvider
結構體,初始化器爲init(_ provider: @escaping ThemePropertyProvider<T>)
。傳入的參數ThemePropertyProvider
是一個閉包,定義爲:typealias ThemePropertyProvider<T> = (ThemeStyle) -> T
。這樣就能夠針對不一樣的控件,不一樣的屬性配置,實現最大化的自定義。 核心代碼參考第一步示例代碼。服務器
對控件添加Associated object
屬性providers
存儲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)
}
//存儲在擴展屬性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> }
}
}
複製代碼
爲了在主題切換的時候,通知到支持主題屬性配置的控件。經過在設置主題屬性時,就記錄目標控件。 核心代碼就是第3步裏面的這句代碼:app
ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
複製代碼
而後它會被記錄到ThemeManager
的trackedHashTable
屬性裏面。由於trackedHashTable
是NSHashTable<AnyObject>.init(options: .weakMemory)
,經過弱引用記錄控件,因此不存在內存問題。ide
經過ThemeManager.changeTheme(to: style)
完成主題切換,方法內部再調用被追蹤的控件的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 ...
}
複製代碼
DarkMode
;theme
命名空間屬性:view.theme.xx = xx
。告別theme_xx
屬性擴展用法;ThemeProvider
傳入閉包配置。根據不一樣的ThemeStyle
完成主題屬性配置,實現最大化的自定義;ThemeStyle
可經過extension
自定義style,再也不侷限於light
和dark
;customization
屬性,做爲主題切換的回調入口,能夠靈活配置任何屬性。再也不侷限於提供的backgroundColor
、textColor
等屬性;overrideThemeStyle
,會影響到其子視圖;ThemeStyle
配置屬性的常規封裝、Plist文件靜態加載、服務器動態加載示例;ThemeStyle
添加自定義styleThemeStyle
內部僅提供了一個默認的unspecified
style,其餘的業務style須要本身添加,好比只支持light
和dark
,代碼以下:
extension ThemeStyle {
static let light = ThemeStyle(rawValue: "light")
static let dark = ThemeStyle(rawValue: "dark")
}
複製代碼
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
是一個提供主題屬性配置的輕量級基礎庫,不限制使用哪一種方式加載資源。下面提供的三個示例僅供參考。
通常的換膚需求,都會有一個UI標準。好比UILabel.textColor
定義三個等級,代碼以下:
enum TextColorLevel: String {
case normal
case mainTitle
case subTitle
}
複製代碼
而後能夠封裝一個全局函數傳入TextColorLevel
返回對應的配置閉包,就能夠極大的減小配置時的代碼量,全局函數以下:
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:
...
}
}
複製代碼
主題屬性配置時的代碼以下:
themeLabel.theme.textColor = dynamicTextColor(.mainTitle)
複製代碼
與常規配置封裝同樣,只是該方法是從本地Plist文件加載配置的具體值,具體代碼參加Example
的StaticSourceManager
類
與常規配置封裝同樣,只是該方法是從服務器加載配置的具體值,具體代碼參加Example
的DynamicSourceManager
類
某些業務需求會存在一個控件有多種狀態,好比選中與未選中。不一樣的狀態對於不一樣的主題又會有不一樣的配置。配置代碼參考以下:
statusLabel.theme.textColor = ThemeProvider({[weak self] (style) in
if self?.statusLabelStatus == .isSelected {
//選中狀態一種配置
if style == .dark {
return .red
}else {
return .green
}
}else {
//未選中狀態另外一種配置
if style == .dark {
return .white
}else {
return .black
}
}
})
複製代碼
當控件的狀態更新時,須要刷新當前的主題屬性配置,代碼以下:
func statusDidChange() {
statusLabel.theme.textColor?.refresh()
}
複製代碼
若是你的控件支持多個狀態屬性,好比有textColor
、backgroundColor
、font
等等,你能夠不用一個一個的主題屬性調用refresh
方法,可使用下面的代碼完成全部配置的主題屬性刷新:
func statusDidChange() {
statusLabel.theme.refresh()
}
複製代碼
無論主題如何切換,overrideThemeStyleParentView
及其子視圖的themeStyle
都是dark
overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
複製代碼
theme
命名空間屬性,而不是使用theme_xx
擴展屬性呢?Kingfisher
、SnapKit
等知名三方庫,都使用了命名空間屬性實現對系統類的擴展,這是一個更Swift
的寫法,值得學習。extension Notification.Name {
public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
}
複製代碼
ThemeManager
根據用戶ID存儲主題配置/// 配置存儲的標誌key。能夠設置爲用戶的ID,這樣在同一個手機,能夠分別記錄不一樣用戶的配置。須要優先設置該屬性再設置其餘值。
public var storeConfigsIdentifierKey: String = "default"
複製代碼
當你的應用最低支持iOS13時,若是須要的話能夠按照以下指南,遷移到系統方案。 遷移到系統API指南,點擊閱讀
最後再複習一下github地址,點擊進入查看更多細節。JXTheme Github地址