写在前面的话

在WWDC2019的时候,我们的尊敬的库克爵士(蒂莫西·唐纳德·库克)公布了iOS13.0系统的升级,其中包括了照片编辑,新的“High-Key Mono”光效,一些常用应用程序的功能性升级,比如地图,提醒,记事...还有我们最重要的深色模式

>> Implementing Dark Mode on iOS <<

 

不对,错了,是这个图。

咳咳咳,总之和Android Q一样,iOS系统现在也可以进行深色模式的切换了,真是可喜可贺,可喜可贺...

然而,真的是这样甜这样简单吗?

我的APP能一键随着系统切换深色模式吗?
我需要书写很多繁重冗余的代码吗?
我的图片会变得怎样,我的图标该怎么办?
我需要进行小细节上的适配吗?
(此处唐僧)...等等,这都是开发者需要关注的问题。

由于笔者最近在做业务需求的时候分配到了这个任务,而且,一开始觉得很简单的东西:差不多工作量1周(?)的FLAG,
直到越深挖越感叹这个坑如此之大。

因此,写出这篇较为全面的深色模式全面适配指南来与各位分享。

容易被混淆的概念

深色模式(Dark Mode),夜间模式(Night Mode),颜色反转(Inverted Colors),这几个词都代表了一些在昏暗环境下能控制贴合人眼浏览体验的效果,那么他们是不是基本类似的?

深色模式 ≠ 夜间模式
要理解深色模式与夜间模式的区别,首先要理解在夜间时屏幕上内容的显示问题。
我们的人眼并不为电子设备而生,对比度过高的内容会不可避免地给人眼带来疲劳。使用过iPhone或者Mac的同学们应该知道他们的自动调节亮度功能,光传感器根据所处环境的亮度帮助电子设备自动调整屏幕亮度。在昏暗时,亮度明显降低,使其对比度相对减少。值得一提的是,对比度也会显著的影响内容可访问性,其需遵循WCAG2.0(Web Content Accessibility Guide 2.0)的AA级标准,即4.5:1的对比度。
夜间模式的重点在于:调整整体亮度,还有内容之间的对比度提供在昏暗环境下良好的浏览体验。
Tips:使用ColorReview进行对比度的检查 - 戳此链接
深色模式 ≠ 颜色反转
而深色模式和颜色反转也有不小的区别。
或许很多同学认为,将一个应用程序页面上的各个元素进行颜色的反色处理,就可以得到深色模式。把白底黑字反转为黑底白字,总体上的确是正确的处理方向,但所有的细节还需商榷。
比如,图片是否应该反色,那应当是不进行反色处理,但是,作为图标的图片又应当进行(前提是纯灰度图片)反色处理。还有,单纯的反色处理还会破环应用程序的页面的层级关系。暂且不管图片反色引起的显示问题,可以看到,原来浅灰色背景上面的列表是纯白色,表现了光照下的层级关系。而此处深色模式下,白色没有单纯被反色为黑色,而是一个较为深的灰色,并且,背景的较为浅的灰色却被反色成为了纯黑色的背景,这保持了原来各个视图层之间的层级关系。

 

 

关于光照环境下表现层级关系的问题,可以想象屏幕上方悬着一盏电灯,离灯越近的视图层被光照得越亮颜色更浅,而离灯越远的视图层缺少光照颜色更深,这表现了空间维度上的层级关系,和现实世界的现象非常符合。
颜色反转的重点在于:将页面上各个元素颜色进行直接反相,以便利地设置为“深颜色”的显示模式。
Tips:iOS系统的的反色处理可以在“设置 > 可访问性 > 智能反转”中设置。

前期计划

那么,深色模式到底来如何进行适配呢?
我们的现在的大前提是,Apple刚提出了iOS13.0的深色模式支持,我们便要进行产品的上线了。
原本最合适的做法应当是,先由视觉设计师进行所有页面的在深色模式状态下的绘制,再由开发进行适配工作,但是现在显然我们没有这么多时间了~这可能牵涉到数百数千个页面,预估好几个月的工作量。
决定配色方案,瞄准灰度层级
首先,第一步需要做的是,我们需要由之前的APP配色方案扩展出深色模式下的配色方案,这里最重要的关注应当是灰度层级。
在图中左边显示的是原来的灰度层级,主要适用于文字(Text),背景(Background)与分隔线(Separate Line),我们先基于这几个颜色进行顺序反转的调整。这样我们就得到了右边的深色模式下灰度层级的配色方案。
但是注意,就像上面在关于反色篇章提出的一样,这样的单纯以顺序反转的调整是不恰当的,这里会破坏一些列表型,卡片型视图的层级关系。因此,我们需要作出一些特例的调整。

 

 

在这里添加2个特例颜色,这主要适用于在亮色模式下,背景为灰,前景为白的页面场景。然后,我们可以在白色的前景视图的基础上,层叠一系列的,一般化的具有层级的灰度。
彩色该怎么办呢

比起上面的灰度层级,彩色部分更好控制,它们几乎不需要在色彩空间中被反转。因此,在这里的建议做法是直接调整它们的饱和度,稍微降低一些即可。因为饱和度较高的颜色容易在深色背景下更为显得刺眼,引起视觉上的疲劳。

 

在深色背景下观察一下各个彩色是否符合预期的显示效果。

整合配色方案,语义化

我们在上面完成了基本的配色方案,现在总算可以把我们新增加的内容整合在一起啦。

以上就是我们这次所订制出来新的配色方案。
特别需要注意的是,每个颜色的左边都有其相对应的名字,我们使用

  • colorGrayLighter;
  • colorGrayLight;
  • colorGrayNormal...

等等来进行各级灰度的层次划分,这是颜色名的语义化标签。

当然,你也可以使用

  • colorGray1;
  • colorGray2;
  • colorGray3...

等较为简单的命名风格来进行划分(Apple就是这么做的,你可以在 UIColor 中找到系统的配色方案)。

但是这里不太推崇这样的方法,来进行颜色的硬性指定

  • colorTitle;
  • colorSubtitle;
  • colorText...

我了解这样做的理由,比如这样的话,作为标题显示的 UILabel 就可以直接使用colorTitle了,的确比较方便管理。
用但是,今后的业务复杂度不太好预测,比如一个custom的 UITableViewCell 中,可能我们含有主标题(title),副标题(subtitle),正文(text),但是,如果今后还有装饰性文字(widget text),或者序号(number),那么强行套用以上的颜色名显然是不合理的,扩充颜色库也会造成不小的麻烦。
所以,这里的语义化(semantic)不应当依附于功能(function),这是我个人的一点看法。

从可视化到代码

现在,我们已经有我们应用程序的配色方案咯。
我们需要将其还原为代码,首先从封装一个简单的类开始吧。

可以在这里下载到Demo工程:

(OC)DarkModeDemoOC
(Swift)DarkModeDemoSwift

UIColor是动态颜色

iOS 13 之前 UIColor 只能表示一种颜色,从 iOS 13 开始 UIColor 是一个动态的颜色,它可以在 LightMode 和 DarkMode 拥有不同的颜色。

UIColor增加了新的初始化方法(OC)colorWithDynamicProvider:与(Swfit)UIColor.init(dynamicProvider:),可以将闭包传入做一些基本的逻辑判断。

Objective-c:

Swift:

简单的配色方案类

在我们的应用程序的工程目录中新建对应的类文件,命名为 AppColorScheme,我们将在这里书写代码。
我们需要罗列出配色方案中的各种颜色。

Objective-c

Swift

我们先参照配色方案,在 APPColorScheme 中申明颜色的静态属性,以及 colorWithDarkMode: 方法,以供各个 UIViewController 使用。

使用方法

有了我们的配色方案类,我们就可以使用它们了,我们可以这样写

Objective-c

Swift

是不是很简单?仅仅替换各个控件的颜色代码就可以。在系统切换深色模式/亮色模式之后,控件的颜色也会随之相应改变。

替换硬编写的颜色代码

所以,深色模式的适配是一件很简单的事情,只要将对应的颜色代码替换就行了。
我们只需要参考配色方案表,将原来的颜色代码替换成 APPColorScheme.colorGrayHeavier 这样的形式。

但是,在我们现在的工程项目中,各个控件的颜色设置可能有这几种情况。

  • 使用UIColor的系统内置颜色(如systemGray1,labelColor等);
  • 使用形如UIColor(red: 0.3, green: 0.4, blue: 0.5, alpha: 0.7)等使用RGB作为参数生成的颜色;
  • 使用形如[UIColor colorWithHex:0x429fde]等自定义方法,配合16进制颜色作为参数生成的颜色。

我们的坏消息是:以上3种情况无一例坏地都需要手动进行颜色替换。
对于较大规模的应用程序项目工程中,可能有有数千处,甚至上万处包含以上的颜色代码。没有什么捷径,一点一点进行替换吧。

一些通用方法

上面我们提到的,colorWithDarkMode: 是一个通用方法,对于特殊情况,可以在外部使用它。
比如,

设计师:我希望这个文字在亮色模式下是橘色的,在暗色模式下是灰色的

工程师:...? 说好的自动按配色方案来呢?

设计师:这样比较好看,特殊情况特殊处理嘛,组织上已经决定了

工程师:得令~

那我们就可以这样写

注意,这样的特例最好不要太多,以后维护起来会变的越来越麻烦。

不起作用的CGColor

很遗憾,CGColor并没有提供对应的功能来自动切换颜色,这种情况常见于为layer.borderColor设置颜色时。

如果设置了:

titleLabel.layer.borderColor = APPColorScheme.colorGrayLighter.CGColor;

在第一次启动APP时,会进行正确的颜色设置,然而,如果在APP处于后台时,在系统设置中进行深色 / 亮色模式的切换的话,是不会自动切换到对应的显示效果的。

在这里的解决方案为如下所示。

在检测到显示模式切换时,重新设置一下对应控件的颜色即可。

替换图片

图片的适配更为简单,请提供对应亮色 / 深色模式下显示的图片即可。

首先找到对应xcasset中的图片。

选中,将其右侧属性栏中的Apperance从默认的None设置为Dark/Any。

 
将准备好的深色模式对应图片拖入即可。

最后的话

让我们来看看效果图吧。

Bingo!
这就是一个适配DarkMode的简单方法。

深色模式的适配方法较为简单,但是对于一个已经存在上线的项目工程来说,工作量无疑是巨大的。
需要有足够的耐心,与...人参时间。

但是,请慢慢来吧~
期待与你下次再见。

参考文章

  1. 一篇吃透 Dark Mode ,搞定“暗黑/深色”适配
  2. Dark Mode 适配工作小指南
  3. 又见 Dark Mode!无线“暗黑”时代,设计师如何接招?
  4. WWDC2019-214-iOS 13 适配 dark mode