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地址

相关文章
相关标签/搜索