iOS13的暗黑模式來了,專案最低支援iOS9怎麼辦?
蘋果爸爸總是讓人又愛又恨啊,今年的暗黑模式註定要讓iOS開發者折騰半天。但是也再次體現了iOS開發者的價值,iOS生態獨特的特性和其不斷的變化與進步,才讓iOS開發者始終被人銘記,不會完全被大前端和多端統一技術給淹沒。從這個角度來說,要感謝蘋果爸爸?
說回正題,iOS13的dark mode相關API只能在iOS13以後才能使用。但是大部分的專案都還是會堅持支援老系統,以獲取更多的使用者。現在網上有許多關於iOS13 dark mode的適配文章,相關的技術點都很簡單。主要的是字型顏色、圖片的適配。看過之後,內心更加悲涼,iOS13 dark mode適配我都會了,老系統腫麼辦呢??
你需要一個輕量級、api友好、高度自定義且最低支援iOS9+的換膚方案。別擔心!我的戰友? ,讓我為你推薦JXTheme方案,它主要借鑑了iOS13的暗黑模式適配API,使用JXTheme你會感到非常親切。而且當你的應用最低支援iOS13時,可以方便的從JXTheme切換到系統API。
Github地址
大家可以先進入github地址,看一下效果。JXTheme Github地址
讓我們從整個暗黑模式適配的流程來熟悉JXTheme的原理:
1.如何優雅的設定主題屬性
通過給控制元件擴充套件名稱空間屬性theme
,類似於SnapKit
的snp
、Kingfisher
的kf
,這樣可以將支援主題修改的屬性,集中到theme
屬性。這樣比直接給控制元件擴充套件屬性theme_backgroundColor
更加優雅。
核心程式碼如下:
view.theme.backgroundColor = ThemeProvider({ (style) in
if style == .dark {
return .white
}else {
return .black
}
})
複製程式碼
2.如何根據傳入的style配置對應的值
借鑑iOS13系統APIUIColor(dynamicProvider: <UITraitCollection) -> UIColor>)
。自定義ThemeProvider
結構體,初始化器為init(_ provider: @escaping ThemePropertyProvider<T>)
。傳入的引數ThemePropertyProvider
是一個閉包,定義為:typealias ThemePropertyProvider<T> = (ThemeStyle) -> T
3.如何儲存主題屬性配置閉包
對控制元件新增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> }
}
}
複製程式碼
4.如何記錄支援主題屬性的控制元件
為了在主題切換的時候,通知到支援主題屬性配置的控制元件。通過在設定主題屬性時,就記錄目標控制元件。 核心程式碼就是第3步裡面的這句程式碼:
ThemeManager.shared.addTrackedObject(self.base,addedConfig: config)
複製程式碼
然後它會被記錄到ThemeManager
的trackedHashTable
屬性裡面。因為trackedHashTable
是NSHashTable<AnyObject>.init(options: .weakMemory)
,通過弱引用記錄控制元件,所以不存在記憶體問題。
5.如何切換主題並呼叫主題屬性配置閉包
通過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 ...
}
複製程式碼
預覽
特性
-
支援iOS 9+,讓你的APP更早的實現
DarkMode
; -
使用
theme
名稱空間屬性:view.theme.xx = xx
。告別theme_xx
屬性擴充套件用法; -
使用
ThemeProvider
傳入閉包配置。根據不同的ThemeStyle
完成主題屬性配置,實現最大化的自定義; -
ThemeStyle
可通過extension
自定義style,不再侷限於light
和dark
; -
提供
customization
屬性,作為主題切換的回撥入口,可以靈活配置任何屬性。不再侷限於提供的backgroundColor
、textColor
等屬性; -
支援控制元件設定
overrideThemeStyle
,會影響到其子檢視; -
提供根據
ThemeStyle
配置屬性的常規封裝、Plist檔案靜態載入、伺服器動態載入示例;
使用示例
擴充套件ThemeStyle
新增自定義style
ThemeStyle
內部僅提供了一個預設的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,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檔案配置示例
與常規配置封裝一樣,只是該方法是從本地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()
}
複製程式碼
overrideThemeStyle
不管主題如何切換,overrideThemeStyleParentView
及其子檢視的themeStyle
都是dark
overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
複製程式碼
其他說明
為什麼使用theme
名稱空間屬性,而不是使用theme_xx
擴充套件屬性呢?
- 如果你給系統的類擴充套件了N個函式,當你在使用該類時,進行函式索引時,就會有N個擴充套件的方法干擾你的選擇。尤其是你在進行其他業務開發,而不是想配置主題屬性時。
- 像
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"
複製程式碼
遷移到系統API指南
當你的應用最低支援iOS13時,如果需要的話可以按照如下指南,遷移到系統方案。 遷移到系統API指南,點選閱讀
Github地址
最後再複習一下github地址,點選進入檢視更多細節。JXTheme Github地址