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. 如何根据传入的样式配置相应的值
参考 iOS13 系统API UIColor(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)
}
/ / Stored in the extended properties provider
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)
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 generic
If let provider = object as? ThemeProvider<UIColor> {
Provider.config?(currentThemeStyle)
}else ...
}
特性
- 支持 iOS 9+,让您的应用更早实现
DarkMode
; - 使用
theme
命名空间属性:view.theme.xx = xx
。告别theme_xx
属性扩展的使用; -
ThemeStyle
可以通过extension
自定义,不再局限于light
和dark
; - 提供
customization
属性作为主题切换的回调入口,具有配置任何属性的灵活性。不再局限于提供的属性,如backgroundColor
和textColor
; - 支持控件设置
overrideThemeStyle
,影响其子视图;
预览
要求
- iOS 9.0+
- XCode 10.2.1+
- Swift 5.0+
安装
手册
克隆代码,将“Sources”文件夹拖入项目,即可使用;
CocoaPods
Target '<Your Target Name>' do
Pod 'JXTheme'
End
首先执行 pod repo update
,然后执行 pod install
Carthage
在 cartfile 中添加
Github "pujiaxin33/JXTheme"
然后执行 carthage update --platform iOS
。对于其他配置,请参阅 Carthage 文档。
#用法
ThemeStyle
添加自定义样式
通过扩展 ThemeStyle
仅提供默认的 unspecified
样式。其他业务样式需要自行添加。例如,只支持 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
/ / You can choose any other property
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 {
/ / selected state a configuration
If style == .dark {
Return .red
}else {
Return .green
}
}else {
//Unselected another configuration
If style == .dark {
Return .white
}else {
Return .black
}
}
})
当控件的州被更新时,你需要刷新当前主题属性配置,代码如下
Func statusDidChange() {
statusLabel.theme.textColor?.refresh()
}
如果你的控件支持多个状态属性,如 textColor
,backgroundColor
,font
等,你可以调用 refresh
方法而不使用主题属性之一。你可以使用以下代码来完成所有配置的主题。属性刷新
Func statusDidChange() {
statusLabel.theme.refresh()
}
重写主题样式
无论主题如何切换,overrideThemeStyleParentView
及其子视图的themeStyle
都是dark
模式
overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
其他技巧
为什么使用主题作用域属性而不是 themed_xx 扩展属性?
- 当你将 N 个函数扩展到系统类中,当你使用这个类时,会有 N 个扩展方法干扰你的选择。特别是当你做其他业务开发时,而不仅仅是当你想配置主题属性。
- 像
Kingfisher
,SnapKit
这样知名的第三方库,都使用名称空间属性来实现对系统类的扩展。这是一种更Swift
表达方式,值得学习。
主题切换通知
Extension Notification.Name {
Public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
}
ThemeManager
根据用户ID存储主题配置
/// Configure the stored flag key. Can be set to the user's ID, so that in the same phone, you can record the configuration of different users. You need to set this property first and then set other values.
Public var storeConfigsIdentifierKey: String = "default"
迁移到系统API指南
当您的应用至少支持iOS13时,如果需要遵循以下指南,您可以迁移到系统计划。[迁移到系统API指南,点击阅读] (https://github.com/pujiaxin33/JXTheme/blob/master/Document/%E8%BF%81%E7%A7%BB%E5%88%B0%E7% B3%BA%E7%BB%9FAPI%E6%8C%87%E5%8D%97.md)
当前支持的类及其属性
这里的属性是继承的。例如,UIView
支持的 backgroundColor
属性,那么其子类 UILabel
也支持 backgroundColor
。如果您没有想要支持的类或属性,欢迎您扩展PullRequest。
UIView
backgroundColor
tintColor
alpha
定制
UILabel
font
textColor
shadowColor
highlightedTextColor
attributedText
UIButton
func setTitleColor(_ colorProvider: ThemeColorDynamicProvider?, for state: UIControl.State)
func setTitleShadowColor(_ colorProvider: ThemeColorDynamicProvider?, for state: UIControl.State)
func setAttributedTitle(_ textProvider: ThemeAttributedTextDynamicProvider?, for state: UIControl.State)
func setImage(_ imageProvider: ThemeImageDynamicProvider?, for state: UIControl.State)
func setBackgroundImage(_ imageProvider: ThemeImageDynamicProvider?, for state: UIControl.State)
UITextField
font
textColor
attributedText
attributedPlaceholder
keyboardAppearance
UITextView
font
textColor
attributedText
keyboardAppearance
UIImageView
image
CALayer
backgroundColor
borderColor
borderWidth
shadowColor
定制
CAShapeLayer
fillColor
strokeColor
UINavigationBar
barStyle
barTintColor
titleTextAttributes
largeTitleTextAttributes
UITabBar
barStyle
barTintColor
UISearchBar
barStyle
barTintColor
keyboardAppearance
UIToolbar
barStyle
barTintColor
UISwitch
onTintColor
thumbTintColor
UISlider
thumbTintColor
minimumTrackTintColor
maximumTrackTintColor
minimumValueImage
maximumValueImage
UIRefreshControl
attributedTitle
UIProgressView
progressTintColor
trackTintColor
progressImage
trackImage
UIPageControl
pageIndicatorTintColor
currentPageIndicatorTintColor
UIBarItem
func setTitleTextAttributes(_ attributesProvider: 主题属性动态提供者?, for state: UIControl.状态)
UIBarButtonItem
tintColor
UIActivityIndicatorView
样式
UIScrollView
指示器样式
UITableView
分割线颜色
分区索引颜色
分区索引背景颜色
Contribution
如果您有任何问题或建议,请随时通过问题和拉取请求与我们联系