优酷暗黑模式(六):暗黑模式的技术支撑 iOS

一、概述

Apple 是从 iOS 13 正式发布了对暗黑模式的支持。

参考文档:

iOS: https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/dark-mode/

苹果在 iOS13 以前已经对自己的部分应用加入了深色模式的支持,比如 iBooks。iBooks 切换到深夜模式后,仍然保持美观的用户界面,对于长时间盯着屏幕的用户而言降低了眼睛的疲劳度。

适配暗黑模式要面临的问题,是如何在深色和浅色模式下界面同时保持应用的视觉效果。涉及各组件的颜色适配,图标的适配,整体的观感,还需要考虑开发的工作量,适配暗黑的方式,以及对现有业务的影响等。

适配暗黑模式和应用换肤的大有区别:对于 App 的部分界面本身具备换肤的能力,切换到一个深色的皮肤并不代表适配了暗黑模式。从用户的角度来看,如果换肤的时机与系统切换暗黑模式是一致的,两者看不出明显的区别。但是对于复杂的像优酷这样大型 App,很难对所有的界面,包括弹窗,提示,H5 页面,提供换肤的能力。从这点看,暗黑模式 SDK 必须是比换肤更简便,更轻量,覆盖更全面的方案。

在优酷 iOS 的暗黑模式开发过程中,我们自行开发了一个“暗黑 SDK”。我们希望达到的目的是,业务代码只需要最少的改动就能适配暗黑模式。

1、前期实践

在开发暗黑 SDK 之前,我们对优酷 APP 中常用的组件和基本控件构建了一套标准化组件库,对于间距,字号,颜色等基础属性,从设计维度提炼出一整套通过 DesignToken 来访问的属性库。标准组件库接入暗黑 SDK 之后,使用了标准组件库的业务代码无需修改就可以直接适配暗黑模式。对于其他使用非标准组件的业务,暗黑 SDK 提供了最简化的接入方案。

2、暗黑 SDK 在 App 中的位置

App 动态颜色的维护和切换由暗黑 SDK 来完成。下图是暗黑 SDK 在 App 层次结构中的位置。

1)暗黑 SDK 的初始化。暗黑 SDK 是在 App 启动时初始化的,主要完成以下工作 :

  • 暗黑模式开关的设置
  • 对各类 View 的相关方法进行注册
  • 装载预置的主题集,比如深色和浅色两种主题
  • 自定义动态颜色的初始化。

2)暗黑 SDK 的工作原理。暗黑 SDK 作为监听系统暗黑模式变更,并通知界面切换颜色以及图片的统一入口,暗黑 SDK 提供所有动态颜色 / 动态图片的 Token 接口,供业务方使用。暗黑 SDK 汇总了全部的动态颜色色值,集中维护,方便将来新增主题。为以后后台管理,下发主题提供了可能性。

当系统的暗黑模式切换时, 暗黑 SDK 监听到模式变更,开始遍历 App 所有窗口,以及窗口下的各类注册过的 View, 然后遍历 View 的注册过的属性。

如果属性的值是动态颜色或者动态图片, 则会根据当前的暗黑模式取对应的颜色或图片, 然后重新赋值。业务代码不用主动刷新页面, 也不用监听当前是不是暗黑模式, 不涉及服务端, 不需要关心当前 App 以哪一种模式运行。

3、暗黑 SDK 更新界面的工作流程:

4、暗黑 SDK 在视图链中搜寻标记了动态属性的节点:

5、暗黑 SDK 的开关以及支持的类型:

6、和苹果官方的暗黑切换的调用过程的对比

通过苹果官方提供的接口分析,当系统的暗黑模式变化时,任何层级的 UIView 的 traitCollectionDidChange 方法都可以被调用到,说明苹果暗黑内部的实现方法必然是一种遍历所有 UIWindow,UIVIewController 及 UIView 的机制,或者类似遍历的机制。

暗黑 SDK 同样使用了遍历所有 UIWindow 的方式,寻找设置了动态颜色或动态图片的节点,来达到在 Light 和 Dark 模式之间切换的效果。

为什么采用遍历的方式?

适配暗黑模式,是不是就是用不同的颜色和图片刷新下界面就可以了?答案是否定的。

刷新界面不能解决适配暗黑的所有问题,还会带来负面影响。

因为适配暗黑模式的工作,主要是对老的业务代码进行修改。如果沿用现有代码逻辑中的刷新逻辑的话,假如刷新中包含数据请求,会导致同时触发了不必要的代码和逻辑。如果刷新导致用户当前的操作现场丢失,是不太好的用户体验。

另外,不是所有代码都存在刷新逻辑,比如一个普通的弹窗,创建的时候数据是固定的,显示后不存在刷新的必要。对于这样的控件,如果为了接入暗黑模式需要新增一个专门的刷新函数的话,对于优酷这样的大型 App 其工作量不可想象。如果存在大量这样的改动,也会带来大量测试的回归工作量。

考虑到这些特殊情况,为了达到暗黑切换的效果且不影响到业务逻辑,最直接和高效的办法是直接修改界面元素的相关属性,比如直接修改 UILabel 的 textColor。遍历整个视图层次链,看似费时费力,实则最大程度地减轻了业务方的工作量,覆盖面相对完整。

二、动态颜色的支持

从最简单的案例开始:

1、上图中的暗黑模式切换涉及到以下两种情况:

1)这个蓝色的按钮在深色和浅色下没有任何变化,文字颜色是白色,背景图保持不变

2)容器的背景色,浅色模式下是白色,深色模式下是黑色,其他按钮,比如,

在浅色模式下文字是黑色,背景是白色,在深色模式下相反。

2、解决方案:

第一种情况:在深色和浅色没有任何变化的代码,保持不变。

第二种情况:实现暗黑模式的切换,需要做如下改动:

涉及到图片的案例

1、上图中的暗黑切换涉及到以下两种情况:

1)按钮的颜色变化,上个示例已经讲过

2)弹窗的背景图的变化,浅色模式下是一张浅红色的图片,深色模式下是一种透明的图片。

解决方案:

实现暗黑模式的切换,需要做如下改动:

从上面的两个例子可以看出,适配暗黑只需要将现有代码中的颜色和图片替换成带 Token 的颜色和图片即可。可以看出 Token 是暗黑切换的关键。

2、Token 的设计

这个表格定义了几种常见的动态颜色的 token。

比如 primaryBackground 是一个背景色的 token,对应两种颜色,在浅色模式下是#FFFFFF 白色,在深色模式下是#16161A 浅白色。

带 token 的动态颜色可以适应暗黑模式的变化。

3、全局暗黑模式开关

考虑到暗黑模式的支持的早期阶段,可能存在解决方案支持不完整,存在一些 bug 的情况,我们添加了全局暗黑模式开关。

这个开关支持

  1. 在特定的机型和版本上可以关闭暗黑开关

  2. 在测试用例中可以选择开启或关闭暗黑开关

3、暗黑 SDK 和组件标准化的关系

暗黑 SDK 处于标准组件库的下一级,为标准组件库提供暗黑方案的支持,同时也为其他自定义组件提供支持。

四、为什么不使用苹果官方的暗黑方案?

为什么我们要独立开发一个“暗黑 SDK”,而不是直接使用苹果的官方方案呢?

苹果官方暗黑方案的几处不太便利的点
案例 苹果官方的方案 暗黑 SDK 方案
1 CGColor 适应暗黑切换 不直接支持, 需要进监听广播 支持
2 自定义属性适应暗黑切换 不直接支持, 需要进监听广播 支持
3 非 UIView 的自定义适应暗黑切换 不直接支持, 需要进监听广播 支持
4 动态颜色的使用 需要区分系统版本 直接使用
5 iOS13 以下适应暗黑切换 不支持 支持

1、CGColor 的暗黑适配

iOS 系统原生的方案:

必须监听广播,取得当前暗黑模式下的 UIColor 的值,然后根据 UIColor 提取 CGColor,代码如下:

复制代码

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[supertraitCollectionDidChange:previousTraitCollection];
UIColor*dyColor = [UIColorcolorWithDynamicProvider:^UIColor* _Nonnull(UITraitCollection* _Nonnull trainCollection) {
if([trainCollection userInterfaceStyle] ==UIUserInterfaceStyleLight) {
return[UIColorredColor];
}
else{
return[UIColorgreenColor];
}
}];
layer.backgroundColor = dyColor.CGColor;
}

暗黑 SDK 的方案:

无需监听,直接赋值,CGColor 也是动态颜色,可以适应暗黑变化,代码如下:

复制代码

layer.backgroundColor= [UIColor.dyColor CGColor];

2、支持自定义属性

复制代码

@interfaceMyView:UIView
@property(nonatomic,strong)UIColor* specialColor;
@end
@implementationMyView
-(void) setSpecialColor:(UIColor*)color{
// 其他代码
self.label.textColor = color;
}
#endif

在 iOS 原生的解决方案中:

复制代码

myView.specialColor= dyColor;

此动态颜色无法适应暗黑变化

如果需要完成暗黑的适配,必须监听暗黑模式变化广播,具体代码如案例 1,增加代码的复杂度和可读性。

在暗黑 SDK 中

复制代码

myView.specialColor= UIColor.dyColor;

此动态颜色可以适应暗黑变化,代码简洁。

3、支持自定义类:

复制代码

@interfaceMyObject: NSObject
@property(nonatomic,strong) UIColor* specialColor;
@end
@implementationMyObject
-(void)setSpecialColor:(UIColor*)color{
// 其他渲染代码

}
#endif

在 iOS 原生的解决方案中:

在 iOS 13 中,UIView、UIViewController,UIWindow 及其子类支持暗黑模式。其他类,比如 NSObject 则不支持暗黑。如果需要完成暗黑的适配,需要在与 MyObject 有关联的 UIView、UIViewController,UIWindow 中监听暗黑模式的变化广播,破坏了代码的模块化设计。

具体代码如案例 1,在回调函数中操作 MyObject,增加了代码的复杂度。

在暗黑 SDK 中:

复制代码

MyObject.specialColor =UIColor.dyColor;

此动态颜色可以适应暗黑变化,代码简洁。

4、iOS 系统的动态颜色只在 iOS13 以上被支持,iOS13 以下无法直接使用:

复制代码

_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property(class,nonatomic,readonly)UIColor*labelColor

在 iOS 原生的解决方案中:

需要针对当前机器的 OS 版本区别对待,或者额外提供另一个函数,封装所有的动态颜色。

复制代码

if#available(iOS 13, *) {
label.textColor = UIColor.labelColor;
} else {
label.textColor = UIColor.blackColor;
}

在暗黑 SDK 中

复制代码

label.textColor= UIColor.dyColor;

此动态颜色可以直接书写,代码简洁。

5、支持 iOS13 以下系统

在 iOS 原生的解决方案中:

不支持 iOS13 以下系统。

在暗黑 SDK 中

支持 iOS13 以下系统。

苹果官方暗黑方案的几处不太便利的点一共五点。

特别是前三点,并不是功能的缺失,只涉及到了代码改动的复杂度,需要判断 OS 版本。

而暗黑 SDK 将代码的改动简化到一行代码,这对于大规模的业务快速接入的优势是显而易见的。

另外,在业务代码加入大量的监听代码,并在监听中判断当前模式是否 UIUserInterfaceStyleLight,可维护性比较差。

五、优酷 APP 在深色模式下的效果图

六、总结

设计标准化的逻辑为暗黑模式的快速接入奠定了基础,暗黑模式的快速实现和上线更凸显了设计标准化的重大意义。

将更多的界面元素统一到标准组件库中,实现组件集中化开发,组件的使用就能够实现跨业务,跨应用,将极大提高业务开发效率和视觉换新的成本。

作者简介:

大甘,阿里文娱无线开发专家。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章