“暗黑模式”之58 同城 iOS App深色模式适配实践

导语

“暗黑模式”最近赚足了话题。自 iOS 13 发布后,深色模式开始赚取眼球,3月22日,微信发布iOS新版本更新,正式支持深色模式。

关于深色模式, 58 同城 App 
最近也做了很多探索和实践。本文总结了 58 同城 App 深色模式适配实践经验与成果,希望对您有所启发,让你的 App 开发的更加赏心悦目。

前言
随着 iOS 13 和 Android 10 的正式发布,深色模式 (Dark Mode) 风靡一时。深色模式带来了酷炫的界面,更让人眼睛看起来非常舒服,如果用一个词来总结深色模式,那就是“赏心悦目”!
目前业界已有不少App适配了深色模式,58 同城 iOS App 经过几个版本的迭代,在几个月前就已经上线深色模式。本文将通过深色模式适配缘由,深色模式适配设计目标,适配成果展示三方面,与大家分享 58 App 深色模式适配的经验与成果。
为什么适配深色模式
深色模式的适配,必然会带来开发成本的增加。要为 58 App 这样一个业务庞大、技术栈多样的应用适配深色模式,并不是一件容易的工作。为什么要适配深色模式,深色模式又能带来怎样的收益,是很多设计师、开发者、产品关心的问题。

1.  苹果推荐

深色模式是 iOS 13 推出的新特性,深色模式为 iOS 系统和各种 app 带来精美的深色配色方案。iOS 系统中,内置的 App 大都已适配深色模式,苹果强烈推荐开发者为自己的 App 适配深色模式。
2. 提升用户体验
深色模式不仅为 App 带来酷炫的深色配色方案,更有助于提升用户体验。在弱光环境下,深色模式的 App 让你的眼睛看起来舒服,而且更专注于内容,同时也不会打扰周围的人。
随着 iOS、Android 在系统层级对深色模式的支持,许多 App 也已逐步适配深色模式,未来越来越多的用户也将习惯并依赖于深色模式。试想一下,一个习惯了深色模式的用户,在一个宁静幽暗夜晚,突然切换到一个闪亮的 App,是多么糟糕的体验!

3. 业界趋势

在苹果推出深色模式之前,许多 App 业已支持夜间模式。谷歌在 Android 10 系统实现了对深色模式的支持,Flutter 1.12 也已完全支持 iOS 13 中的深色模式。随着 iOS、Android 两大手机操作系统对深色模式的完美支持,深色模式适配逐渐成为业界趋势,为自己的 App 适配深色模式也成为一项必要的工作。
目前业界的许多应用,包括淘宝 App、百度 App、QQ、爱奇艺、优酷等都已适配深色模式,就连曾不忍心占用用户珍贵夜晚的微信,也已向用户认怂,上线了深色模式。
4. 节省电量
深色模式不仅能为用户带来赏心悦目的体验,还能够节省电量,改善电池寿命。在 OLED 屏幕的手机(包括 iPhone 11 Pro, iPhone XS, and iPhone X)上,效果更加明显。
OLED 屏幕通过关闭相应的像素来产生纯黑色显示。这意味着在深色模式下,屏幕上的所有黑色区域都将被完全关闭,从而节省电池电量。
YouTube PhoneBuff 频道发布了一个实验视频,对比了 iPhone XS Max 在相同亮度下,浅色模式和深色模式下的电池使用情况。经过数小时的使用后,深色模式 iPhone 的电池电量剩余 43%,明显高于浅色模式 iPhone 的电池电量的 20%;当浅色模式的 iPhone 电量耗尽后,深色模式的 iPhone 电池电量还剩余 30%。
适配设计目标
对于 58 App 这样一个业务庞杂的大型App,适配深色模式并不是一件容易的工作。深色模式的适配涉及首页、部落、热议、招聘、租房、二手房、二手车、黄页、passport 等诸多业务线。58 App 的技术栈也相当复杂,页面包括 Navtive 页面、H5 页面、RN 页面,都需要适配深色模式。另外版本迭代节奏比较快,平均三周一个版本,要在一两个版本完成整个 App 的深色模式适配也不现实,我们通过几个版本的迭代,目前主路径页面已经完成深色模式适配,对于暂时未能适配深色模式的页面,通过自动添加蒙层,保障用户体验。为了减轻后续业务迭代成本,提高开发效率,我们设计样式库,逐步推广标准化组件。58 App 的深色模式适配设计目标主要考虑了以下几方面内容:

1. 如何应对复杂技术栈

58 App页面从技术上可以分为三类,Native 页面、RN 页面、Web 页面,三种页面深色模式适配技术上相互独立,却又紧密联系,整体流程设计如下图所示:


图1 流程设计图
首先三类页面中,都存在由于开发资源和项目时间等因素限制,暂时未能适配深色模式的页面。为保证这些页面正常显示,让用户获得更好的用户体验,同时减轻业务线开发成本,由 Native 端统一添加蒙层。当 App 打开深色模式开关时,如果页面未适配深色模式,则自动添加蒙层;当跳转到适配深色模式的页面时,则移除蒙层。具体实现方案见“如何兼容不能参与适配功能”小节,此处不再赘述。
下面分别介绍 Native 页面、RN 页面、Web 页面深色模式适配技术方案:

Ø  Native 页面深色模式适配

为了兼容暂时未能适配的页面能够正常显示,我们通过 Runtime Method Swizzling 将页面默认重置为浅色样式。对于需要适配深色模式的页面,开发者首先需要在 viewDidLoad 方法中重置页面样式,使当前页面支持深色模式,示例代码如下:

- (void)viewDidLoad{

    [super viewDidLoad];

    if (@available(iOS 13.0, *)) {

        self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;

    }

}

在页面支持深色模式后,接下来主要工作为修改视图背景色,文字颜色以及图片。
为提高开发效率,同时考虑后续业务迭代扩展成本,我们开发了样式库,样式库实现了颜色与图片的统一管理,规范了 UI 标准,为深色模式适配奠定了基础,具体设计参见“如何应对后续业务迭代成本”小节。

样式库提供了简单易用的 API,修改视图背景色,代码示例如下:

view.backgroundColor = [StyleLib colorWithType:@"Primary_1"];

为 UIImage 设置背景图片,示例如下:

UIImage* backImage = [StyleLib iconWithType:@"icon_back "];

Ø  RN页面深色模式适配

RN 页面为统一的载体页,在初始化时,Native 侧通过跳转协议或 Action 协议中相关参数,识别当初页面是否支持深色模式,设置当前载体页样式。同时将当前 App 样式,发消息给 RN 侧,渲染页面。
RN 组件适配深色模式,示例如下:

const styles = StyleSheet.create(

    { text: { fontSize: 16, color: ‘black’ } },

    { text: { color: ‘white’ } });


//fontSize: 16, color: ‘white’

RN 侧同时监听 App 显示状态,当 App 显示状态切换时,Native侧将当前样式发送至 RN 侧,RN 侧刷新渲染页面,实现逻辑如下:

MYAPP . addListener( ‘custom_dark_mode’ , mode => { 

    // 方案二:重启RN应用

    MYAPP . applyUpdate( )

    // 方案三:刷新RN应用

    this . setState( { visible: false } ) ;

    setTimeout ( ( ) => {

    this . setState( { visible: true } ) ;

}, 0 ) ; });

方案二:重启RN应用。所有状态丢失,⻚面重新渲染。
方案三:刷新RN应用。原有状态保留,⻚面重新渲染。

Ø  Web页面深色模式适配

Web 页面同样在初始化时,通过跳转协议或 Action 协议参数,发送给 Hybrid 载体页,识别当初页面是否支持深色模式,设置当前载体页样式。Web 页实现相对简单,自己能够监听 App 显示状态,使用如下:

function myFunction(x) {

    if (x.matches) { // 媒体查询

        MYAPP.action.toast('dark')

    } else {

        MYAPP.action.toast('light')

    }

}

var x = window.matchMedia("(prefers - color - scheme: dark)") 

// 执行时调用的监听函数

myFunction(x)

// 状态改变时添加监听器 模式修改的时候触发

x.addListener(myFunction)

2. 如何应对大型 App 并行研发
58 App 迭代速度较快,通常三周一个版本。深色模式适配项目涉及首页、部落、热议、招聘、租房、二手房、二手车、黄页、passport 等诸多业务线,其中每个业务线有自已的日常业务开发和资源限制,另外随着 App 工厂项目的持续推进,一些基础组件和通用业务可能需要平移到其他应用。如何处理 58 App 中多业务线并行研发,甚至基础组件和通用业务的跨应用平移,都是在项目中要解决的问题。
为支撑多业务线并行研发,提高开发效率,深色模式适配框架分为基础层、服务层、业务层三层,架构设计如下:

图2  
架构设计图

基础层定义了样式库,主要包括颜色样式库、图片样式库、主题库,实现了图片、颜色、主题的统一管理,提供了简洁易用的 API,为深色模式适配奠定了基础。
服务层主要包括蒙层管理、跳转协议与 action 协议、控制开关。蒙层管理用于为暂时不支持深色模式的页面自动添加蒙层,优化用户体验;跳转协议、action 协议中定义了 RN 侧、Web 侧与 Native 端交互参数,用于控制页面显示样式。
控制开关在多业务并行研发过程中发挥着重要作用,控制开关分为 App 级与页面级两个粒度,能够灵活控制 App 及页面样式,为项目质量提供保障。项目开发初期,由于整体页面适配覆盖率较低,为保障用户体验,使用 App 级控制开关临时关闭深色模式。随时项目持续推进,整体效果碰到预期,我们再打开 App 级控制开关,如果仅有个别页面效果不佳,可以使用页面级控制开关,暂时关闭。


图3 深色模式控制开关
58 App 中许多基础组件及通用业务,要随着 App 工厂项目平移到其他应用,在深色模式适配过程中,要考虑不同 App 间主题的兼容有以及其他应用开发编译环境兼容。关于基础组件主题样式间的兼容,可以参考“如何应对后续业务迭代成本”小节样式库的介绍。
在深色模式适配开发中,需要使用一些 iOS 13 新增的 API,如 setOverrideUserInterfaceStyle,如果不加编译控制,在低版本 Xcode 中,会导致编译失败。58 App 中许多公共组件,需要平移至集团其他 App 中,为兼容低版本 Xcode 编译,在使用 iOS 13 API 时,增加以下编译控制判断:

#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0

    if (@available(iOS 13.0, *)) {

      self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;

    }

#endif

3. 如何兼容不能参与适配功能
对于 58 App 这样大型复杂的应用,在短期内将所有的页面完成深色模式适配不太现实。由于各业务线有自己的开发任务和资源限制等因素,部分页面暂时不能支持适配。如何保障这些未适配的页面能够正常显示,并且最大可能的提升深色模式下用户体验,是我们设计过程中思考的一个重要问题。

Ø  兼容未适配深色模式的页面正常显示

Xcode 11默认开启了深色模式,一些使用系统默认颜色的视图,在切换到深色模式时,可能出现显示异常,如下图所示:


图4 显示异常示意图
针对类似问题,苹果官方提供了解决方案,暂时不能支持深色模式适配的控制器、视图,可以将其 overrideUserInterfaceStyle 属性,设置为 UIUserInterfaceStyleLight 样式。解决方法看似简单,但要推动所有业务线去修改页面样式却并非易事,并且实施过程中还有可能遗漏。
为减轻业务线适配压力,提高开发效率,经过项目组讨论,最终我们选择使用 Objective-C  Runtime中的黑魔法 method swizzling 。通过 hook viewDidLoad 方法,在交换方法中,默认将 ViewController的overrideUserInterfaceStyle 属性重置为浅色样式,示例代码如下:

- (void)my_viewDidLoad{

    if (@available(iOS 13.0, *)) {

        if(NO == [self isSystemController]){

            self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;

        }

    }

    [self my_viewDidLoad];

}

通过上述方案,确保即使未适配深色模式的页面,在用户切换到深色模式时,也能够正常显示。
需要适配深色的模式的页面,在相应的 viewDidLoad 方法中,重置 overrideUserInterfaceStyle 属性为默认样式即可。

在这里需要注意的是,不能简单粗暴的将所有的控制器重置为浅色样式。项目中会使用图片选择、文档选择等系统提供的功能,这些控制器已支持深色模式。为解决该问题,我们建立了名单管理机制,过滤掉那些以 UI 或下划线开头的系统类以及一些有特殊需求的业务类,使用默认样式。
另一个需要说明的问题是,上述代码只是重置了 ViewController 的样式,保证页面正常显示。一些直接添加到 Window 上视图仍然可能出现显示异常,在此同样可以选择 hook View 的相关方法,重置视图为浅色样式。Method swizzling 虽然功能强大,但还是尽可能选择慎用。经过权衡我们选择在工程是全局搜索添加到 window 上的相关代码,未能适配深色模式的视图,手动设置为浅色样式。

Ø 
 
为未适配深色模式页面添加蒙层

在保证未适配深色模式的页面和视图能够显示正常显示后,为优化深色与浅色页面跳转过程中的用户体验,我们为未适配深色模式的页面添加了蒙层。


图5 蒙层效果图
添加蒙层需要关注三个问题:
1)如何区分当前页面是否适配深色模式
要为未适配的页面添加蒙层,首先要区分哪些页面已适配深色模式,哪些页面未适配深色模式。我们已通过方法交换将所有页面默认重置为浅色样式,同时适配深色模式的页面,需要手动设置为默认样式。因此我们可以根据控制器的 overrideUserInterfaceStyle 属性判断一个页面,是否支持深色模式。
2)蒙层添加位置
在页面上添加蒙层,有两种方案:一是为每个控制器添加蒙层,这种方案比较灵活,便于页面自由定制,对于一些嵌套的控制器,需要特殊处理,实现起来相对复杂;另一种方案是在 Window 上添加蒙层,该方案实现起来比较简单,而且能够完全覆盖整个屏幕,体验更佳。因此我们选择在 Window 上添加蒙层。
3)蒙层显示隐藏时机
蒙层显示可以在 viewWillAppear 方法中,判断当前页面是否支持深色模式,如果不支持深色模式,则给当前页面添加蒙层;在 viewDidAppear 方法中,判断如果当前页面支持深色模式,则隐藏蒙层。
另外还需要监听系统显示模式切换,当系统切换到浅色模式时,移除蒙层;若系统切换到深色模式,并且当前控制器未适配深色模式,则需要显示蒙层。
4. 如何应对后续业务迭代成本
深色模式的适配需要修改调整大量页面和组件的颜色,如果仅仅为了适配深色模式,将所有的 UI 颜色修改一遍,这样耗时费力的修改工作的收益,值得我们思考。能否提供一套基础库,提升深色模式适配开发效率,同时减轻后续业务迭代成本,是我们设计过程中重点关注的另一个问题。
基于上述思考,我们设计了样式库。样式库不仅提供了简单易用的 API,方便业务方快速调整 UI 样式,适配深色模式;同时实现了 UI 样式的统一管理,让 UI 规范化、标准化,另外兼顾了 UI 组件标准化及跨应用平移,后续业务迭代成本及扩展,让深色模式适配项目更具价值。
样式库整体设计如下:


图6 样式库设计

Ø 
 
简单易用的 API

首先样式库,提供了一套简洁易用的 API,为深色模式的适配奠定了基础,提高了开发效率。在没有样式库之前,一个视图适配深色模式,调整背景色,示例代码如下:

view.backgroundColor = [UIColor lightColorWithHex:0xD9E2E9 lightColorAlpha:0.5 darkColorWithHex:0xFFFFFF darkColorAlpha:0.0];

基于样式库,修改视图背景色时,使用语义颜色,无论是否需要设置透明度,都可以这样写:

view.backgroundColor = [StyleLib colorWithType:@"Primary_1"];

Ø  
颜色统一管理,灵活扩展换肤

在未使用样式库之前,颜色值硬编码于代码之中,修改组件颜色,需要大范围查找替换。
样式库对语义颜色与实际色值做映射存储,并对颜色实现了统一管理,让 UI 更加标准规范。
业务方不再允许使用硬编码的色值,而是直接使用语义颜色。
基于样式库,后续如果需要调整色值,只在样式库层调整即可,不再需要到处修改代码,减轻了后续修改成本。

样式库的建立同时为后期主题扩展提供了可行性,如果要扩展其他主题,只需要在样式库底层扩展主题,而业务层不需要大范围的修改,即可实现App 灵活换肤。

Ø  
一次编码,多处运行

随着 58 App 工厂项目的快速持续推进,UI组件的跨业务复用,甚至跨应用平移实践场景也越来越多。通常不同的应用有着独自的设计风格,为实现 UI 组件的跨应用平移,样式库设计过程中,不同的 App 分别对应独立的主题库。真正实现了UI 组件的一次编码,多处运行,极大减轻了 UI 组件跨应用平移成本。
在深色模式适配项目中,我们已经实现了导航栏、通用弹窗、分享面板、Loading 框、筛选器等 UI 组件的标准化,后续我们将实现更多 UI 组件的标准化,推动业务方接入,减轻后续业务迭代及平移成本。

Ø  
多套皮肤,一套设计

样式库不仅提高了开发效率,而且极大减轻了设计师的设计成本,对于多套主题,设计师只需提供一套设计。
样式库实现了颜色的统一管理,使 UI 更加标准规范,为深色模式的快速接入奠定了基础,深色模式的快速实现和上线更凸显了样式库的重大意义。将更多的 UI 组件统一到标准组件库中,实现组件集中化开发,UI 组件就能够实现跨业务,跨应用复用,极大提高业务开发效率和降低后续业务迭代及平移的成本。
适配成果展示及性能优化

1. Native 页面


图7  Native页面

2. RN 页面


图8  RN页面

3. Web 页面


图9  Web页面

4. 蒙层效果


图10  蒙层效果
5. 耗电量对比
由于OLED屏幕中每个像素都是自主发光而非LCD由整个一块背光面板发光,所以在显示深色元素时像素所消耗的电流更低,尤其在纯黑颜色时像素点可以完全关闭达到省电的效果。目前,只有iPhone X、XS、XS MAX、11 Pro、11 Pro MAX 五款苹果手机使用的是OLED屏幕。
为此针对58同城App适配深色模式后对首页做了对比试验:

试验条件:

设备:iPhoneX  系统:iOS13.3
方法:打开58同城首页,设置Timer触发首页列表来回滚动。每掉电1%  就记录一下耗时。

试验结果:

横坐标:时间(分)
纵坐标:单位耗电量

试验结论:

亮色模式下28分钟耗电约为:7.08 %
深色模式下28分钟耗电约为:5.32 %
深色模式下,基于58同城首页,整体耗电量相比亮色模式节约25%左右。
总结
深色模式是公司内部从设计到技术,一切从用户体验角度出发计划并实施的产物。为了让用户在58业务场景里在深色模式下有一致的体验,设计和技术上从Native页面到RN、Hybird页面进行了全面的适配。
在实施的过程中,遇到了很多困难。比较重要的难点是58业务较多,这个适配过程需要不同的业务线设计和技术进行沟通与协作。为了解决这些问题,设计上,统一了集团不同App间设计图上关于色值的定义与描述;同时技术上引入样式库,统一了App内不同业务线间对色值的使用方式,简化了亮色和暗色下调用,并彻底解决了长久以来不同业务间对颜色随意使用的局面。这样,使得用户在使用58App的过程中,具有一致的视觉体验。
后续,为了进一步提升用户体验,会和设计一同进行组件库的构建,组件库构建后不但能降低各业务线对组件适配深色模式的工作量,而且能统一App内组件的风格。而且对一些细节,在深色模式下会进一步打磨。因此,深色适配是其实是一个持续的项目。相信不久的将来,会积累更多深色模式的经验和心得分享给大家。
上述就是58 同城 App 深色模式适配目前的实践经验,由于作者水平有限,文中难免有疏漏之处,欢迎大家交流指正!
参考文献
1. WWDC: Implementing Dark Mode on iOS

2. 
Supporting Dark Mode in Your Interface

3. 
Choosing a Specific Interface Style for Your iOS App

4. 
Switching your phone to dark mode could be a game-changer for your battery life, especially if you have one of 3 iPhones

作者简介
贾学文,58 同城 – 基础技术部 – iOS 技术部 高级研发工程师
蒋演,58 同城 – 基础技术部 – iOS 技术部 架构师

阅读推荐


1. 开源|Zucker:Android APP模块化大小自动分析统计工具


2. 开源|WBBlades:基于Mach-O文件解析的APP分析工具

5.

独家|浅谈对象序列化