QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

DKNightVersion 的实现 --- 如何为 iOS 应用添加夜间模式

  • 2019-12-10
  • 本文字数:3242 字

    阅读完需:约 11 分钟

DKNightVersion 的实现 --- 如何为 iOS 应用添加夜间模式

最新: 成熟的夜间模式解决方案


注意: 这篇文章已经过时了, 最新版本的 2.3.0 实现已经更改了.


在很多重阅读或者需要在夜间观看的软件其实都会把夜间模式当做一个 App 所需要具备的特性. 而如何在不改变原有的架构, 甚至不改变原有的代码的基础上, 就能为应用优雅地添加夜间模式就成为一个在很多应用开发的过程中不得不面对的一个问题.


就是以上事情的驱动, 使我思考如何才能使用一种优雅并且简洁的方法解决这一问题.


DKNightVersion 就是我带来的解决方案.


到目前为止, 这个框架的大部分的工作都已经完成了, 或许它现在不够完善, 不过我会持续地维护这个框架, 帮助饱受实现夜间模式之苦的工程师们解决这个坑的一逼的需求.

实现

现在我也终于有时间来水一水写一篇博客来说一下这个框架是如何实现夜间模式的, 它都有哪些特性.


在很长的一段时间我都在想如何才能在不覆写 UIKit 控件的基础上, 为 iOS App 添加夜间模式. 而 objc/runtime 为我带来了不覆写 UIKit 就能实现这一目的的希望.

为 UIKit 控件添加 nightColor 属性

因为我们并不会子类化 UIKit 控件, 然后使用 @property 为它的子类添加属性. 而是使用 Objective-C 中神奇的分类(Category) 和 objc/runtime, 为 UI 系列的控件添加属性.


使用 objc/runtime 为分类添加属性相信很多人都知道而且经常在开发中使用了. 如果不了解的话, 可以看这里.


DKNightVersion 为大多数常用的 color 比如说: backgroundColor tintColor 都添加了以 night 开头的夜间模式下的颜色, nightBackgroundColor nightTintColor.


Objective-C


- (UIColor *)nightBackgroundColor {    return objc_getAssociatedObject(self, &nightBackgroundColorKey) ? :self.backgroundColor);}
- (void)setNightBackgroundColor:(UIColor *)nightBackgroundColor { objc_setAssociatedObject(self, &nightBackgroundColorKey, nightBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}
复制代码


我们创建这个属性以保存夜间模式下的颜色, 这样当应用的主题切换到夜间模式时, 将 nightColor 属性存储的颜色赋值给对应的 color, 但是这会有一个问题. 当应用重新切换回正常模式时, 我们失去了原有正常模式的 color.

添加 normalColor 存储颜色

为了解决这一问题, 我们为 UIKit 控件添加了另一个属性 normalColor 来保存正常模式下的颜色.


Objective-C


- (UIColor *)normalBackgroundColor {    return objc_getAssociatedObject(self, &normalBackgroundColorKey);}
- (void)setNormalBackgroundColor:(UIColor *)normalBackgroundColor { objc_setAssociatedObject(self, &normalBackgroundColorKey, normalBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}
复制代码


但是保存这个颜色的时机是非常重要的, 在最开始的时候, 我的选择是直接覆写 setter 方法, 在保存颜色之前存储 normalColor.


Objective-C


- (void)setBackgroundColor:(UIColor *)backgroundColor {    self.normalBackgroundColor = backgroundColor;    _backgroundColor = backgroundColor;}
复制代码


然而这种看似可以运行的 setter 其实会导致视图不会被着色, 设置 color 包括正常的颜色都不会有任何的反应, 反而视图的背景颜色一片漆黑.


由于上面这种方法行不通, 我想换一种方法使用观察者模式来存储 normalColor, 将实例自己注册为 color 属性的观察者, 当 color 属性变化时, 通知 UIKit 控件本身, 然后, 把属性存到 normalColor 属性中.


然而在什么时候将自己注册为观察者这一问题, 又使我放弃了这一解决方案. 最终选择方法调剂来解决原有 color 的存储问题.


使用方法调剂为原有属性的 setter 方法添加钩子, 在方法调用之前, 将属性存储起来, 用于切换回 normal 模式时, 为属性赋值.


这是要与 setter 调剂的钩子方法:


Objective-C


- (void)hook_setBackgroundColor:(UIColor*)backgroundColor {    if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {        [self setNormalBackgroundColor:backgroundColor];    }    [self hook_setBackgroundColor:backgroundColor];}
复制代码


如果当前是 normal 模式, 就会存储 color, 如果不是就会直接赋值, 如果你看不懂为什么这里好像会造成无限递归, 请看这里, 详细的解释了方法调剂是如何使用的.

DKNightVersionManager 实现 color 切换

我们已经为 UIKit 控件添加了 normalColornightColor, 接下来我们需要实现 color 在这两者之间的切换, 而这 DKNightVersionManager 就是为了处理模式切换的类.


通过为 DKNightVersionManager 创建一个单例来处理 模式转换, 使用默认颜色, 动画时间 等操作.


当调用 DKNightVersionManager 的类方法 nightFalling 或者 dawnComing 时, 我们首先会获取全局的 UIWindow, 然后通过递归调用 changeColor 方法, 使能够响应 changeColor 方法的视图改变颜色.


Objective-C


- (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {    if ([object respondsToSelector:@selector(changeColor)]) {        [object changeColor];    }    if ([object respondsToSelector:@selector(subviews)]) {        if (![object subviews]) {            // Basic case, do nothing.            return;        } else {            for (id subview in [object subviews]) {                // recursice darken all the subviews of current view.                [self changeColor:subview];                if ([subview respondsToSelector:@selector(changeColor)]) {                    [subview changeColor];                }            }        }    }}
复制代码


因为我在这个类中并没有引入 category, 编译器不知道 id 类型具有这两个方法. 所以我声明了一个协议, 使 changeColor 中的方法来满足两个方法 changeColorsubViews. 不让编译器提示错误.


Objective-C


@protocol DKNightVersionChangeColorProtocol <NSObject>
- (void)changeColor;- (NSArray *)subviews;
@end
复制代码


然后让所有的 UIKit 控件遵循这个协议就可以了, 当然我们也可以不显式的遵循这个协议, 只要它能够响应这两个方法也是可以的.

实现默认颜色

我们要在 DKNightVersion 实现默认的夜间模式配色, 以便减少开发者的工作量.


但是因为我们对每种 color 只在父类中实现一次, 这样使得子类能够继承父类的实现, 但是同样不想让 UIKit 系子类继承父类的默认颜色.


Objective-C


- (UIColor *)defaultNightBackgroundColor {    BOOL notUIKitSubclass = [self isKindOfClass:[UIView class]] && ![NSStringFromClass(self.class) hasPrefix:@"UI"];    if ([self isMemberOfClass:[UIView class]] || notUIKitSubclass) {        return UIColorFromRGB(0x343434);    } else {        UIColor *resultColor = self.normalBackgroundColor ?: [UIColor clearColor];        return resultColor;    }}
复制代码


通过使用 isMemberOfClass: 方法来判断实例是不是当前类的实例, 而不是该类子类的实例. 然后才会返回默认的颜色. 但是非 UIKit 中的子类是可以继承这个特性的, 所以使用这段代码来判断该实例是否是非 UIKit 的子类:


Objective-C


[self isKindOfClass:[UIView class]] && ![NSStringFromClass(self.class) hasPrefix:@"UI"]
复制代码


我们通过 NSStringFromClass(self.class) hasPrefix:@"UI" 巧妙地达到这一目的.

使用 erb 生成 Objective-C 代码

这个框架大多数的工作都是重复的, 但是我并不想为每一个类重复编写近乎相同的代码, 这样的代码十分不易阅读和维护, 所以使用了 erb 文件, 来为生成的 Objective-C 代码提供模板, 只将元数据进行解析然后传入每一个模板, 动态生成所有的代码, 再通过另一个脚本将所有的文件加入目录中.




DKNightVersion 的实现并不复杂. 它不仅使用了 erb 和 Ruby 脚本来减少了大量的工作量, 而且使用了 objc/runtime 的特性来魔改 UIKit 组件, 达到为 iOS 应用添加夜间模式的效果.


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/dknightversion


2019-12-10 17:52753

评论

发布
暂无评论
发现更多内容

MQTT保留消息是什么?如何使用?

EMQ映云科技

物联网 IoT mqtt 企业号 2 月 PK 榜 保留消息

华为云API Arts:用“1+1+5”的模式,为你带来API-First体验

华为云开发者联盟

云计算 后端 华为云 企业号 2 月 PK 榜 华为云开发者联盟

为什么面试 SaaS 产品经理一定要问权限管理?

产品海豚湾

产品经理 SaaS 权限管理 B端 产品面试

到底怎么理解分布式事务

做梦都在改BUG

京东力荐!深入理解高并发编程手册,GitHub上线3小时飙升榜首

做梦都在改BUG

Java 并发编程 高并发

体验AI乐趣:基于AI Gallery二分类猫狗图片分类小数据集自动学习

华为云开发者联盟

人工智能 华为云 企业号 2 月 PK 榜 华为云开发者联盟

苏宁基于 AI 和图技术的智能监控体系的建设

NebulaGraph

运维 图数据库

BSN-DDC基础网络详解(三):注册门户账号和业务开通(1)

BSN研习社

墨天轮发布数据库行业报告,亚信科技AntDB“超融合+流式实时数仓”开启新纪元

亚信AntDB数据库

数据库 AntDB 国产数据库 AntDB数据库 企业号 2 月 PK 榜

【NeurIPS 2022】视频动作识别,AFNet 用更低的成本接收更多数据

Zilliz

MQTT持久会话与Clean Session详解

EMQ映云科技

物联网 IoT mqtt 企业号 2 月 PK 榜 持久会话

佛萨奇2.0智能合约矩阵公排系统开发源代码(可改链)

开发微hkkf5566

MQTT QoS 0,1,2介绍

EMQ映云科技

物联网 IoT mqtt QoS 企业号 2 月 PK 榜

微服务-常见配置中心工作原理

做梦都在改BUG

Java 微服务 配置中心

热点面试题:JS 中 call, apply, bind 概念、用法、区别及实现?

Immerse

JavaScript call apply bind 前端面试题

为什么推荐一个容器只运行一个进程?

追赶者

k8s 为什么

项目上线后我是如何通过慢查询和索引让系统快起来的

做梦都在改BUG

MySQL 数据库 索引

Matic链矩阵公排智能合约挖矿dapp系统开发详情(案例演示)

开发微hkkf5566

flutter系列之:在flutter中使用导航Navigator

程序那些事

flutter 大前端 程序那些事

LeetCode:240. 搜索二维矩阵 II,二分查找,详细注释

Lee Chen

JavaScript 算法 LeetCode

分布式事务系统Seata的这些安保机制是否会让你更放心

做梦都在改BUG

Java 分布式 Spring Boot seata

修改ctags让fzf.vim插件显示C,C++方法声明的标签

Geek_pwdeic

ctags fzf.vim

软件测试/测试开发 | 想做App测试就一定要了解的App结构

测试人

软件测试 自动化测试 测试开发 app测试

app上架需要准备什么以及上架流程

雪奈椰子

TestRai、Testlink、Jira、PingCode等6款测试用例管理工具对比

爱吃小舅的鱼

管理工具 测试用例管理工具

借力英特尔® Smart Edge,灵雀云 ACP 5G 专网解决方案获得多维度优化加速

York

云原生 5G 系统架构 边缘计算 英特尔

带你动手设计一个高速公路多节点温度采集系统

华为云开发者联盟

云计算 物联网 华为云 企业号 2 月 PK 榜 华为云开发者联盟

在TitanIDE中使用ChatGPT辅助科研开发

行云创新

AI 云端开发 TitanIDE

软件测试/测试开发 | app测试中常用的Android模拟器

测试人

android 软件测试 自动化测试 测试开发

火山引擎DataTester:在广告投放场景下的A/B实验实践

字节跳动数据平台

大数据 AB testing实战 企业号 2 月 PK 榜

DKNightVersion 的实现 --- 如何为 iOS 应用添加夜间模式_语言 & 开发_Draveness_InfoQ精选文章