写点什么

成熟的夜间模式解决方案

  • 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:58734

评论

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

JProfiler 14 for Mac( Java 应用程序性能分析工具)

Mac相关知识分享

作为 Java 程序员必须要掌握的技术栈

架构师之道

Java 编程

长三角,如何把数据要素变成新长江?

脑极体

AI

基于Flexus X实例云服务器的评测-大模型对比评测

YG科技

Bartender 4 for Mac(菜单栏应用管理软件)

Mac相关知识分享

使用内容分发网络的一点小经验

为自己带盐

腾讯云 CDN

6年Java开发经验,面试20多家公司,砍下10多个Offer(附面经分享)

采菊东篱下

编程 java面试

混合云架构中私网环境下通过VPN实现云上云下互通

天翼云开发者社区

混合云

Acrobat Pro DC 2021 for Mac(pdf编辑器)

Mac相关知识分享

Java 面试都只是背答案吗?

了不起的程序猿

后端 架构师 java程序员 java面试 Java八股文

具身智能领域,全球Top50华人图谱梳理

机器人头条

科技 大模型 人形机器人 具身智能

如何系统全面地自学Java语言?

了不起的程序猿

后端 自学编程 架构师 java程序员 java面试

AutoCAD 2024 for Mac(cad设计绘图工具)

Mac相关知识分享

Premiere Pro 2021 for Mac(pr 2021 直装版)

Mac相关知识分享

快讯|复旦校友会、浙大MBA、中欧EMBA来访奇点云

奇点云

打破生态壁垒!ThinkPad X1 Carbon Aura AI“一磕即传”刷新多设备协作效率

科技范儿

华为云Flexus X实例使用测评——上手初体验,比想象的更丝滑

YG科技

828华为云征文 |Flexus X实例与华为云EulerOS的Tomcat安装指南

YG科技

为什么那么多带GC的语言,只有JVM需要调优?

了不起的程序猿

程序员 JVM 架构师 java面试 jvm调优

Microsoft Office 2019 for Mac(office办公套件)

Mac相关知识分享

你的理由都是借口:那些所谓的网络抖动

采菊东篱下

Java 面试 网络

华为云Flexus云服务器X实例的购买及使用体验

YG科技

如何选择合适的云服务器--X实例购买指南和配置详细说明

YG科技

VXLAN 网络中报文转发过程

天翼云开发者社区

VXLAN

基于Flexus X实例云服务器的实际场景-等保三级服务器设置

YG科技

服务网格的基本概念

天翼云开发者社区

服务网格

华为Flexus云服务器X实例 使用流程

YG科技

AnyGo for Mac(虚拟定位软件)

Mac相关知识分享

QCN9274 and QCN6274 Chips-Exploring the Potential -Unleashing the Power of AI in WiFi 7

wifi6-yiyi

WiFi7

Spring事务管理深度解析-从实践到原理

EquatorCoco

Java 数据库 spring

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