苹果爸爸总是让人又爱又恨啊,今年的暗黑模式注定要让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 ...
}
复制代码
预览
<figcaption></figcaption>
特性
- [x] 支持iOS 9+,让你的APP更早的实现
DarkMode
; - [x] 使用
theme
命名空间属性:view.theme.xx = xx
。告别theme_xx
属性扩展用法; - [x] 使用
ThemeProvider
传入闭包配置。根据不同的ThemeStyle
完成主题属性配置,实现最大化的自定义; - [x]
ThemeStyle
可通过extension
自定义style,不再局限于light
和dark
; - [x] 提供
customization
属性,作为主题切换的回调入口,可以灵活配置任何属性。不再局限于提供的backgroundColor
、textColor
等属性; - [x] 支持控件设置
overrideThemeStyle
,会影响到其子视图; - [x] 提供根据
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, 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文件加载配置的具体值,具体代码参加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地址
作者:金字塔程序员
链接:https://juejin.im/post/5d882ff85188253ec722e21f