JXTheme:iOS9+換膚/暗黑模式最佳方案之一,輕量級、高度自定義、swift編寫

簡介

2018年蘋果在macOS系統引入了暗黑模式,一經推出廣受好評。尤爲是咱們程序員,常常與代碼、文本打交道,亮色風格的界面看久了,眼睛會特別累。有了暗黑模式以後,咱們的眼睛終於能被溫柔對待了。並且系統內置的應用適配的很是好,拿咱們經常使用的XCode來講,也挑不出什麼大毛病。反正我是用了暗黑模式以後就沒有回去過了。git

固然推出暗黑模式不僅是爲了程序員準備的,也有其餘的緣由:程序員

  • 能夠當作夜間模式:晚上看屏幕的時候,不會亮到你睜不開眼睛。
  • 信息重點的表達須要:在黑色系更能突出關鍵信息,能作到一目瞭然,抓住用戶的焦點。
  • 用戶審美的須要:有至關多的用戶對黑色系的產品很鍾愛,固然要迎合他們的需求了。
  • 硬件設備省電的須要:如今流行的OLED屏幕,對於純黑色像素點是不須要通電的。

其實無論它有多少緣由,蘋果爸爸這麼大力的推廣,確定有它的價值,咱們跟着蘋果爸爸走的就行。這不iOS13就引入到了iOS系統,對於咱們開發者來講,就是又愛又恨啊。若是大家的產品是有格調的產品,多半暗黑模式適配的需求就在路上了,就像我同樣😂。可是最尷尬的地方在於,適配暗黑模式的api只在iOS13可用,你又要讓我適配暗黑模式,又要讓我最低支持iOS9,你這不是讓我爲難嗎🤷?沒辦法系統原生支持不了的,那就到我們的寶藏網站Github上面找一找iOS9+的換膚方案。固然找到了許多,大部分是OC的,由於項目主要語言是Swift,因此pass掉。找到了許多swift三方庫,可是裏面的一些設計有點過期、有些不支持swift五、有些功能過重了。我只想要一個輕量級、高度自定義的方案便可。沒有現成的知足的庫,與其委曲求全,不如本身實現。因此就有了JXTheme方案,一個輕量級、api友好、高度自定義的換膚方案。github

該方案主要借鑑了iOS13的暗黑模式適配API,因此建議你先去網上查閱iOS13的暗黑模式適配指南。先對系統的方案有必定了解,再來看JXTheme你會感到很是親切。關鍵在於JXTheme最低支持iOS9,等於說在iOS9就能使用iOS13的暗黑模式適配方案。並且後面還給出了當你的應用最低支持iOS13時,從JXTheme切換到系統API的指南。swift

Github地址

你們能夠先進入github地址,看一下效果。JXTheme Github地址api

核心代碼&關鍵流程介紹

下面按照換膚API的調用流程來介紹實現方案bash

1.如何優雅的設置主題屬性

經過給控件擴展命名空間屬性theme,相似於SnapKitsnpKingfisherkf,這樣能夠將支持主題修改的屬性,集中到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。 核心代碼以下:app

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步裏面的這句代碼:ide

ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
複製代碼

而後它會被記錄到ThemeManagertrackedHashTable屬性裏面。由於trackedHashTableNSHashTable<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 ...
}
複製代碼

預覽

preview

特性

  • 支持iOS 9+,讓你的APP更早的實現DarkMode;
  • 使用theme命名空間屬性:view.theme.xx = xx。告別theme_xx屬性擴展用法;
  • 使用ThemeProvider傳入閉包配置。根據不一樣的ThemeStyle完成主題屬性配置,實現最大化的自定義;
  • ThemeStyle可經過extension自定義style,再也不侷限於lightdark;
  • 提供customization屬性,做爲主題切換的回調入口,能夠靈活配置任何屬性。再也不侷限於提供的backgroundColortextColor等屬性;
  • 支持控件設置overrideThemeStyle,會影響到其子視圖;
  • 提供根據ThemeStyle配置屬性的常規封裝、Plist文件靜態加載、服務器動態加載示例;

使用示例

擴展ThemeStyle添加自定義style

ThemeStyle內部僅提供了一個默認的unspecifiedstyle,其餘的業務style須要本身添加,好比只支持lightdark,代碼以下:

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文件配置示例

常規配置封裝同樣,只是該方法是從本地Plist文件加載配置的具體值,具體代碼參加ExampleStaticSourceManager

根據服務器動態添加主題

常規配置封裝同樣,只是該方法是從服務器加載配置的具體值,具體代碼參加ExampleDynamicSourceManager

有狀態的控件

某些業務需求會存在一個控件有多種狀態,好比選中與未選中。不一樣的狀態對於不一樣的主題又會有不一樣的配置。配置代碼參考以下:

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()
}
複製代碼

若是你的控件支持多個狀態屬性,好比有textColorbackgroundColorfont等等,你能夠不用一個一個的主題屬性調用refresh方法,可使用下面的代碼完成全部配置的主題屬性刷新:

func statusDidChange() {
    statusLabel.theme.refresh()
}
複製代碼

overrideThemeStyle

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

overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
複製代碼

其餘說明

爲何使用theme命名空間屬性,而不是使用theme_xx擴展屬性呢?

  • 若是你給系統的類擴展了N個函數,當你在使用該類時,進行函數索引時,就會有N個擴展的方法干擾你的選擇。尤爲是你在進行其餘業務開發,而不是想配置主題屬性時。
  • KingfisherSnapKit等知名三方庫,都使用了命名空間屬性實現對系統類的擴展,這是一個更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地址

相關文章
相關標籤/搜索