iOS13的暗黑模式來(lái)了,項(xiàng)目最低支持iOS9怎么辦?

蘋果爸爸總是讓人又愛(à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,類似于SnapKitsnp、Kingfisherkf,這樣可以將支持主題修改的屬性,集中到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ì)被記錄到ThemeManagertrackedHashTable屬性里面。因?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ù)覽

preview

<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,不再局限于lightdark;
  • [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需要自己添加,比如只支持lightdark,代碼如下:

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文件加載配置的具體值,具體代碼參加ExampleStaticSourceManager

根據(jù)服務(wù)器動(dòng)態(tài)添加主題

常規(guī)配置封裝一樣,只是該方法是從服務(wù)器加載配置的具體值,具體代碼參加ExampleDynamicSourceManager

有狀態(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容