【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

成熟的夜间模式解决方案

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

评论

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

vue3实战-完全掌握ref、reactive

yyds2026

Vue 前端

SREWorks v1.1 版本发布 | 组件插拔场景化部署能力

阿里云大数据AI技术

大数据 运维 存储 企业号 2 月 PK 榜

百度CTO王海峰:深度学习平台+大模型,夯实产业智能化基座

飞桨PaddlePaddle

人工智能 深度学习 飞桨 PaddlePaddle

不会用“函数选项模式”的朋友看过来,这么写很优雅

王中阳Go

Go golang 高效工作 学习方法 后端

如何应对突发的流量激增和服务器过载问题

NGINX开源社区

nginx 流量 应用交付 企业号 2 月 PK 榜

碎片化NFT系统开发模式定制智能合约部署详情

开发微hkkf5566

webpack配置优化,让你的构建速度飞起

Geek_02d948

中创中间件:基于鲲鹏DevKit开发统一监管平台,性能提升55%

Geek_2d6073

阿里云物联网平台业务Topic规划最佳实践——实践类

阿里云AIoT

小程序 物联网 存储 智能硬件 数据格式

Kyligence 出席全球人工智能开发者先锋大会并成功主办分论坛

Kyligence

数据服务 人工智能大会

2022年度中国一级市场融资事件数量下降52% ;红杉中国、腾讯投资最活跃|2022年全球投融资年报

创业邦

InstructPix2Pix:指挥机器修改图像

Zilliz

webpack热更新原理(面试大概率会问)

Geek_02d948

JavaScript 前端

堡垒机采购注意事项说明-行云管家

行云管家

网络安全 数据安全 堡垒机

vue中的几个高级概念

yyds2026

Vue 前端

大家一起来找茬,新手第一次layout到底能挑出多少毛病?

华秋PCB

工具 PCB 电路板 layout PCB设计

[触觉智能RK3568]指定单个OpenHarmony镜像进行独立编译

离北况归

OpenHarmony

京东前端二面常见vue面试题及答案

yyds2026

Vue 前端

NGINX QUIC & HTTP/3 最新进展

NGINX开源社区

nginx HTTP 企业号 2 月 PK 榜

Memblaze 联合 OpenCloudOS 完成技术兼容互认证

OpenCloudOS

Linux SSD

基于阿里云物联网平台,用20元体验物联网开发( 自制 Arduino 环境监测仪)——实践类

阿里云AIoT

监控 物联网 存储 传感器 测试技术

植根中国,开放合作,英特尔中国开源技术委员会成立

科技之家

如何整理自己的前端面试题库

Geek_02d948

JavaScript 前端

利用规则引擎的M2M实现设备之间联动——实践类

阿里云AIoT

小程序 物联网 智能硬件 网络性能优化

通用信息抽取技术UIE产业案例解析,Prompt范式落地经验分享!

飞桨PaddlePaddle

nlp 模型 情感分析 飞桨 PaddlePaddle

拥抱下一代前端工具链-Vue老项目迁移Vite探索

京东科技开发者

前端 vite 迁移 Vue 3 企业号 2 月 PK 榜

分布式锁实现原理与最佳实践

阿里技术

分布式锁

滴滴前端常考vue面试题

yyds2026

Vue 前端

跬智信息全新推出云原生数据底座玄武,助力国产化数据服务再次升级

Kyligence

云原生 大数据分析

软件测试 | 持续集成的开源方案攻略(二)jenkins pipeline

测吧(北京)科技有限公司

测试

阿里前端二面常考react面试题(必备)

xiaofeng

前端 React

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