1. 程式人生 > IOS開發 >iOS13的暗黑模式來了,專案最低支援iOS9怎麼辦?

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,類似於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。 核心程式碼如下:

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)
複製程式碼

然後它會被記錄到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,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地址