写点什么

成熟的夜间模式解决方案

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

    阅读完需:约 15 分钟

成熟的夜间模式解决方案

关注仓库,及时获得更新:iOS-Source-Code-Analyze


从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。


其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。


在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。


我们会以对 backgroundColor 为例说明整个框架的工作原理。


方法调剂的版本

如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。


其核心思路就是使用方法调剂修改 backgroundColor 的存取方法

使用 nightBackgroundColor

在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在分类中添加 nightBackgroundColor 属性,并且使用方法调剂改变 backgroundColor 的 setter 方法。


Objective-C


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


在当前主题为 DKThemeVersionNormal 时,将颜色保存至 normalBackgroundColor 中,然后再调用原 backgroundColor 的 setter 方法,更新视图的颜色。

DKNightVersionManager

这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。


整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要工作就是负责改变应用的主题、并在主题改变时通知其它视图更新颜色


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]) {                // recursive darken all the subviews of current view.                [self changeColor:subview];                if ([subview respondsToSelector:@selector(changeColor)]) {                    [subview changeColor];                }            }        }    }}
复制代码


如果主题更新,那么就会递归地调用 changeColor 方法,刷新全部的视图颜色,而这个方法的实现比较简单:


Objective-C


- (void)changeColor {    if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {        self.backgroundColor = self.normalBackgroundColor;    } else {        self.backgroundColor = self.nightBackgroundColor;    }}
复制代码


上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:


  1. 在高速滚动的 scrollView 上面来回切换夜间模式,会出现颜色错乱的问题

  2. 由于对 backgroundColor 属性进行不合适的方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同

  3. 无法适配第三方 UI 控件

使用色表的版本

为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 nightBackgroundColor 的使用,并且重新设计底层的实现,转而使用更为稳定安全的方法实现夜间模式,先看一下效果图:



<em>新的实现不仅能够支持夜间模式,而且能够支持多主题。</em>
复制代码

DKColorPicker

与上一个版本实现上的不同,在 2.0 中删除了全部的 nightBackgroundColor,使用一个名为 dk_backgroundColorPicker 的属性取代它。


Objective-C


@property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;
复制代码


这个属性其实就是一个 block,它接收参数 DKThemeVersion *themeVersion,但是会返回一个 UIColor *


在第一次传入 picker 或者每次主题改变时,都会将当前主题 DKThemeVersion 传入 picker 并执行,然后,将得到的 UIColor 赋值给对应的属性 backgroundColor 更新视图颜色。


Objective-C


typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);
复制代码


比如下面使用 DKColorPickerWithRGB 创建一个临时的 DKColorPicker


  1. DKThemeVersionNormal 时返回 0xffffff

  2. DKThemeVersionNight 时返回 0x343434

  3. 在自定义的主题下返回 0xfafafa (这里的顺序与色表中主题的顺序有关)


Objective-C


cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);
复制代码


同时,每一个对象还持有一个 pickers 数组,来存储自己的全部 DKColorPicker


Objective-C


@interface NSObject ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
@end
复制代码


在第一次使用这个属性时,当前对象注册为 DKNightVersionThemeChangingNotificaiton 通知的观察者。


在每次收到通知时,都会调用 night_update 方法,将当前主题传入 DKColorPicker,并再次执行,并将结果传入对应的属性 [self performSelector:sel withObject:result]


Objective-C


- (void)night_updateColor {    [self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull selector, DKColorPicker  _Nonnull picker, BOOL * _Nonnull stop) {        SEL sel = NSSelectorFromString(selector);        id result = picker(self.dk_manager.themeVersion);        [UIView animateWithDuration:DKNightVersionAnimationDuration                         animations:^{#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks"                             [self performSelector:sel withObject:result];#pragma clang diagnostic pop                         }];    }];}
复制代码


也就是说,在每次改变主题的时候,都会发出通知。

DKColorTable

虽然我们在上面临时创建了一些 DKColorPicker。不过在 DKNightVersion 中,我更推荐使用色表,来减少相同的 DKColorPicker 的创建,并且能够更好地管理整个应用中的颜色:


Objective-C


NORMAL   NIGHT    RED#ffffff  #343434  #fafafa BG#aaaaaa  #313131  #aaaaaa SEP#0000ff  #ffffff  #fa0000 TINT#000000  #ffffff  #000000 TEXT#ffffff  #444444  #ffffff BAR
复制代码


上面就是默认色表文件 DKColorTable.txt 中的内容,其中,第一行表示主题,NORMAL 主题必须存在,而且必须为第一列,而最右面的 BGSEP 就是对应 DKColorPicker 的 key。


Objective-C


self.tableView.dk_backgroundColorPicker =  DKColorPickerWithKey(BG);
复制代码


在使用时,上面的代码就相当于返回了一个在 NORMAL 时返回 #ffffffNIGHT 时返回 #343434 以及 RED 时返回 #fafafaDKColorPicker

pickerify

虽然说,我们使用色表以及 DKColorPicker 解决了,但是,到目前为止我们还没有解决第三方框架的问题。


比如我们使用了某个第三方框架,或者自己添加了某个 color 属性,比如说:


Objective-C


@interface DKView ()
@property (nonatomic, strong) UIColor *weirdColor;
@end
复制代码


weirdColor 并没有对应的 DKColorPicker,但是,我们可以通过 pickerify 在想要使用 dk_weirdColorPicker 的地方生成这个对应的 picker:


Objective-C


@pickerify(DKView, weirdColor);
复制代码


然后,我们就可以使用 dk_weirdColorPicker 属性了:


Objective-C


view.dk_weirdColorPicker = DKColorPickerWithKey(BG);
复制代码


pickerify 其实是一个宏:


Objective-C


#define pickerify(KLASS, PROPERTY) interface \    KLASS (Night) \    @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \    @end \    @interface \    KLASS () \    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers; \    @end \    @implementation \    KLASS (Night) \    - (DKColorPicker)dk_ ## PROPERTY ## Picker { \        return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \    } \    - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \        objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \        [self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\        [self.pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \    } \    @end
复制代码


这个宏根据传入的类和属性名,为我们生成了对应 picker 的存取方法,它也可以说是一种元编程的手段。


这里生成的 setter 方法不是标准意义上的驼峰命名法 dk_setweirdColorPicker:,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。

嵌入式 Ruby

由于框架中很多的代码,都是重复的,所以在这里使用了嵌入式 Ruby 模板来生成对应的文件 color.m.irb


Objective-C


////  <%= klass.name %>+Night.m//  <%= klass.name %>+Night////  Copyright (c) 2015 Draveness. All rights reserved.////  These files are generated by ruby script, if you want to modify code//  in this file, you are supposed to update the ruby code, run it and//  test it. And finally open a pull request.
#import "<%= klass.name %>+Night.h"#import "DKNightVersionManager.h"#import <objc/runtime.h>
@interface <%= klass.name %> ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
@end
@implementation <%= klass.name %> (Night)
<% klass.properties.each do |property| %><%= """- (DKColorPicker)dk_#{property.name}Picker { return objc_getAssociatedObject(self, @selector(dk_#{property.name}Picker));}
- (void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker { objc_setAssociatedObject(self, @selector(dk_#{property.name}Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); self.#{property.name} = picker(self.dk_manager.themeVersion); [self.pickers setValue:[picker copy] forKey:@\"#{property.setter}\"];}""" %><% end %>
@end
复制代码


这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 generator 文件夹,其中包含了代码生成器的全部代码。

小结

如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 README 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,AFNetworkingBlocksKit 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。


关注仓库,及时获得更新:iOS-Source-Code-Analyze


本文转载自 Draveness 技术博客。


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


2019-12-10 17:58981

评论

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

Alluxio 2.8版本重磅发布!3大提升抢先打开数据新世界

Alluxio

分布式缓存 数据管理 Alluxio 大数据 开源

前端 “一键换色“ 的几种方案

CRMEB

IDEA常用快捷键总结

爱好编程进阶

程序员 后端开发

java IO流

爱好编程进阶

Java 程序员 后端开发

Open Harmony移植:build lite配置目录全梳理

华为云开发者联盟

HarmonyOS Open Harmony移植 Open Harmony build lite配置

web前端培训interface和type的区别分析

@零度

typescript 前端开发

百亿级数据同步,如何基于 SeaTunnel 的 ClickHouse 实现?

Apache SeaTunnel

Apache 大数据 开源 DolphinScheduler workflow

Java Swing图形化编程之JTextArea

爱好编程进阶

Java 程序员 后端开发

网站开发进阶(三十二)HTML5之FileReader的使用

No Silver Bullet

html5 5月月更 FileReader

Git 常用命令

平凡人生

社交电商如何规避传销风险

源字节1号

软件开发

比心云平台基于阿里云容器服务 ACK 的弹性架构实践

阿里巴巴云原生

阿里云 容器 云原生 客户案例 阿里云容器服务ACK

HR面试都会问什么问题?(上

爱好编程进阶

程序员 后端开发

中科凡语周玉:用我们这一代人的努力扛起国产NLP发展大旗

硬科技星球

Java 之基础程序设计

爱好编程进阶

Java 程序员 后端开发

2013年,我有点喜欢编程了

勇哥java实战分享

什么是隐私计算,它是怎样保护我们的隐私安全

华为云开发者联盟

安全 联邦学习 隐私计算 隐私安全 可信智能计算服务

呵呵,JavaScript 真好玩(苦笑脸)

java培训定时任务的理解与实战教学

@零度

定时任务 JAVA开发

“操作系统的「冷板凳」要坐多久?”万字长文解读16年开源人的坚持

OpenAnolis小助手

Linux 开源 操作系统 坚持 龙蜥社区

Java EE 阶段小项目(小型商城商品展示 + 购物车

爱好编程进阶

Java 程序员 后端开发

电商行业客户服务的解决方案

小炮

全面解析企评家网站功能

企评家

企业评价 企评家 企业成长性评价 企业查询

Java 将字节数组转化为16进制的多种方案

爱好编程进阶

程序员 后端开发

Java 并发编程——AQS 源码学习

爱好编程进阶

Java 程序员 后端开发

构建面向特征工程的数据生态 ——拥抱开源生态,OpenMLDB全面打通MLOps生态工具链

第四范式开发者社区

人工智能 机器学习 数据库 特征平台 特征工程

Kubernetes 1.24 - 走向成熟的 Kubernetes

Daocloud 道客

Kubernetes 云原生 容器编排

Python 操作 Excel 第3篇博客,python openpyxl 模块一文打通

梦想橡皮擦

5月月更

解析Java-throw抛出异常详细过程

华为云开发者联盟

Java 开发语言 throw throws 系统自动抛异常

石油储运生产 2D 可视化,组态应用赋能工业智慧发展

一只数据鲸鱼

数据可视化 数字孪生 组态 石油开采

大数据培训MySQL 索引B+Tree

@零度

大数据

成熟的夜间模式解决方案_语言 & 开发_Draveness_InfoQ精选文章