JXTheme:iOS9+换肤/暗黑模式最佳方案之一,轻量级、高度自定义、swift编写
简介
2018年苹果在macOS系统引入了暗黑模式,一经推出广受好评。尤其是我们程序员,经常与代码、文本打交道,亮色风格的界面看久了,眼睛会特别累。有了暗黑模式之后,我们的眼睛终于能被温柔对待了。而且系统内置的应用适配的非常好,拿我们常用的XCode来说,也挑不出什么大毛病。反正我是用了暗黑模式之后就没有回去过了。
当然推出暗黑模式不只是为了程序员准备的,也有其他的原因:
- 可以当做夜间模式:晚上看屏幕的时候,不会亮到你睁不开眼睛。
- 信息重点的表达需要:在黑色系更能突出关键信息,能做到一目了然,抓住用户的焦点。
- 用户审美的需要:有相当多的用户对黑色系的产品很钟爱,当然要迎合他们的需求了。
- 硬件设备省电的需要:现在流行的OLED屏幕,对于纯黑色像素点是不需要通电的。
其实不管它有多少原因,苹果爸爸这么大力的推广,肯定有它的价值,我们跟着苹果爸爸走的就行。这不iOS13就引入到了iOS系统,对于我们开发者来说,就是又爱又恨啊。如果你们的产品是有格调的产品,多半暗黑模式适配的需求就在路上了,就像我一样?。但是最尴尬的地方在于,适配暗黑模式的api只在iOS13可用,你又要让我适配暗黑模式,又要让我最低支持iOS9,你这不是让我为难吗??没办法系统原生支持不了的,那就到咱们的宝藏网站Github上面找一找iOS9+的换肤方案。当然找到了许多,大部分是OC的,因为项目主要语言是Swift,所以pass掉。找到了许多swift三方库,但是里面的一些设计有点过时、有些不支持swift5、有些功能太重了。我只想要一个轻量级、高度自定义的方案即可。没有现成的满足的库,与其委曲求全,不如自己实现。所以就有了JXTheme
方案,一个轻量级、api友好、高度自定义的换肤方案。
该方案主要借鉴了iOS13的暗黑模式适配API,所以建议你先去网上查阅iOS13的暗黑模式适配指南。先对系统的方案有一定了解,再来看JXTheme
你会感到非常亲切。关键在于JXTheme
最低支持iOS9,等于说在iOS9就能使用iOS13的暗黑模式适配方案。而且后面还给出了当你的应用最低支持iOS13时,从JXTheme
切换到系统API的指南。
Github地址
大家可以先进入github地址,看一下效果。JXTheme Github地址
核心代码&关键流程介绍
下面按照换肤API的调用流程来介绍实现方案
1.如何优雅的设置主题属性
通过给控件扩展命名空间属性theme
,类似于SnapKit
的snp
、Kingfisher
的kf
,这样可以将支持主题修改的属性,集中到theme
属性。这样比直接给控件扩展属性theme_backgroundColor
更加优雅。
核心代码如下:
view.theme.backgroundColor = ThemeProvider({ (style) inif 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 ThemeWapper where Base: UIView {var backgroundColor: ThemeProvider<UIColor>? {set(new) {if new != nil {let baseItem = self.baselet config: ThemeCustomizationClosure = {[weak baseItem] (style) inbaseItem?.backgroundColor = new?.provider(style)}//存储在扩展属性providers里面var newProvider = newnewProvider?.config = configself.base.providers["UIView.backgroundColor"] = newProviderThemeManager.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 = styleself.trackedHashTable.allObjects.forEach { (object) inif 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 ...
}
预览
特性
- 支持iOS 9+,让你的APP更早的实现
DarkMode
; - 使用
theme
命名空间属性:view.theme.xx = xx
。告别theme_xx
属性扩展用法; - 使用
ThemeProvider
传入闭包配置。根据不同的ThemeStyle
完成主题属性配置,实现最大化的自定义; ThemeStyle
可通过extension
自定义style,不再局限于light
和dark
;- 提供
customization
属性,作为主题切换的回调入口,可以灵活配置任何属性。不再局限于提供的backgroundColor
、textColor
等属性; - 支持控件设置
overrideThemeStyle
,会影响到其子视图; - 提供根据
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) inif style == .dark {return .white}else {return .black}
})
imageView.theme.image = ThemeProvider({ (style) inif 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 normalcase mainTitlecase subTitle
}
然后可以封装一个全局函数传入TextColorLevel
返回对应的配置闭包,就可以极大的减少配置时的代码量,全局函数如下:
func dynamicTextColor(_ level: TextColorLevel) -> ThemeProvider<UIColor> {switch level {case .normal:return ThemeProvider({ (style) inif 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) inif 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地址
JXTheme:iOS9+换肤/暗黑模式最佳方案之一,轻量级、高度自定义、swift编写相关推荐
- 前端 “一键换肤“ 的几种方案
作者:熊的猫 原文:https://juejin.cn/post/7063010855167721486 前端 "一键换肤" 的几种方案 前言 现在越来越多的网站都提供了拥有换肤( ...
- 前端 “一键换肤“ 的 N 种方案
前端瓶子君,关注公众号 回复算法,加入前端编程面试算法每日一题群 前言 现在越来越多的网站都提供了拥有换肤(切换主题)功能,如 ElementUI[2],既是为了迎合用户需求,或是为了凸显自己特点,因 ...
- 前端 一键换肤 的几种方案
现在越来越多的网站都提供了拥有换肤(切换主题)功能,如 ElementUI,既是为了迎合用户需求,或是为了凸显自己特点,因此提供了个性化定制的功能. 其实之前就想了解和实现 "一键换肤&qu ...
- React Native 暗黑模式适配方案
Android设备在 10 之后提供了对于暗黑模式的支持, iOS也在 iOS13之后提供了同样的支持.React Native 在 0.62 版本中增加了对于暗黑模式功能的支持,用来提升App用户体 ...
- iOS客户端节日换肤方案探究
转自:https://www.ianisme.com的博客 一.前言: tip: 本来这篇文章在圣诞节就已经准备好了,但是由于种种原因一直没有写完,今天将它写出来,也算是2018年的第一篇文章了.你好 ...
- iOS拓展---【转载】iOS客户端节日换肤方案探究
[转载]iOS客户端节日换肤方案探究 一.前言: Tip: 本来这篇文章在圣诞节就已经准备好了,但是由于种种原因一直没有写完,今天将它写出来,也算是2018年的第一篇文章了.你好,2018! 过去圣诞 ...
- Android三种换肤方案原理及Demo
方案一:使用主题文件 定义换肤资源 在values/下新建一个xml文件,比如theme_attrs.xml,然后定义换肤的资源类型 <?xml version="1.0" ...
- 一文读懂HarmonyOS服务卡片怎么换肤
作者:zhenyu,华为软件开发工程师 关注HarmonyOS的小伙伴肯定对服务卡片已经很熟悉了.服务卡片(也简称为"卡片")是FA(FeatureAbility,元服务)的一种界 ...
- Android应用内换肤
换肤简介 换肤本质上是对资源的一种替换包括.字体.颜色.背景.图片.大小等等.比如View的修改背景颜色setBackgroundColor,TextView的setTextSize修改字体等等. 换 ...
最新文章
- vue2.0组件生命周期探讨
- 这5种员工,千万不能重用
- Less 混合(mixin)
- 快速入门人工智能的方法,持续更新ing
- 关于XGB.booster()报错TypeError: 'str' object is not callable的解决方法
- 在苹果Mac上Word、Excel 界面变黑如何解决?
- FastDFS原理及入门
- 音频特征----频谱图
- ubuntu20.04安装Strom集群
- xamp:在shell中运行mysql
- 解决java.util.ConcurrentModificationException:null
- 学习大数据可以进入哪些公司?
- appserver安装教程
- 基于SSM的在线音乐播放网站音乐网站MP3下载网站(idea-javaweb-php-netC#-j2ee-springboot)上传发布新歌分享评价收藏投票歌单歌手个性化每日推荐算法-排行榜
- ##Windows 10纯净版下载##
- 机器学习 2.1.1 最小二乘估计
- 再呆半天,明天去富士康...
- 漏洞扫描器的扫描原理 ZZ
- 银行卡开户银行、名称缩写等信息查询
- Web安全之充分利用 X-Content-Type-Options
热门文章
- thinkphp ajax 跨域请求 Access-Control-Allow-Origin 完美解决
- (三)JVM成神路之全面详解执行引擎子系统与JIT即时编译原理
- C++ opencv视频处理与保存
- 室内眩光测量UGR测试系统
- 奶酪巫师的黑客乐园 - 第一个进行硬分叉的区块链游戏?
- mysql 导出指定分隔符_mysql 导出行数据到txt文件,指定字符分割
- 如何限制在制品?| Kanban
- 关于requests.exceptions.SSLError: HTTPSConnectionPool(host='XXX', port=443)问题
- 数据链路层-点对点通信方式
- Unity3D项目Plugins目录组织