京东 App适配 iOS 暗黑模式业务实践

Alt

如下文章来源于京东零售技术,做者平台研发姚琦app

什么是暗黑模式?

iOS 13 苹果推出了暗黑模式,暗黑模式在夜间能够更好的保护视力,也能够节省 App 电量消耗。可是 Apple 提供的暗黑模式只支持 iOS 13,为了给用户带来更好的体验,咱们但愿 iOS 13 如下的系统也能够支持暗黑模式。另外咱们还给用户提供了自主选择的权利,能够在 App 内手动关闭暗黑模式,不跟随系统主题变化。异步

京东 App 涉及业务模块众多,整个适配工做量巨大,为了解决上述问题,并让各模块经过统一的接口快速接入,咱们开发了暗黑基础组件,提供如下能力:ide

  • 支持 iOS 9 及以上系统,同时兼容 iOS 13 系统暗黑模式
  • 支持总体切量、降级
  • 支持跟随系统模式,也能够选择不跟随,使用 App 内部的模式
  • 内置调试工具,帮助开发者快速调试,提高效率
  • 支持颜色模式扩展

基础组件设计方案以下:
Alt工具

业务接入spa

业务接入时须要调用基础组件提供的jdbappearance_bindUpdater方法,传入一个Block并在其中处理UI更新的逻辑,基础组件会绑定Block和UIView,而后将UIView存储在HashTable中,在合适的时机经过遍历HashTable和执行绑定的Block来更新UI。业务组件的接入方案以下:设计

Alt

须要注意的是,遍历HashTable的时候并非全部的Block都会执行,这里会判断UIView的window是否存在,若是window有值,就执行UIView绑定的Block,不然会先把这个Block标记为稍后执行,当UIView下次出如今window中时(didMoveToWindow 被调用的时候)就会执行这个Block。3d

另外不用担忧Block会在每次 didMoveToWindow 时被调用,由于只有颜色模式变化的时候,Block才会被标记为稍后执行。调试

若是涉及接口调用等异步场景,是否会增长接入成本呢?咱们经过下面的代码示例看一下业务是如何进行适配的:code

// 接入前
 cell.viewA.backgroundColor = [UIColor redColor];
 cell.viewB.image = [UIImage imageNamed:@"xxx"];
 
 
 // 接入后
 @weakify(cell)
 [cell jdbappearance_bindUpdater:^(JDBAppearance *apperance, UIView *bindView) {
     @strongify(cell)
    cell.viewA.backgroundColor = [UIColor jdbappearance_colorBR];
    cell.viewB.image = [UIImage jdbappearance_imageNamed:@[@"light_xx", @"dark_xx"]];
}];

由于每次调用jdbappearance_bindUpdater 时,会马上执行一次Block,因此不管是否涉及异步场景,接入方式都是统一的,并不会带来额外的接入成本。对象

自定义Updater:

Block机制基本能够知足全部的适配场景,可是实际开发中,咱们可能但愿有一些便捷的方法,好比直接调用一个方法jd_setBackgroundColor设置UIView的背景色。

这样的需求也是能够知足的,咱们来看一下如何封装这样的API:

@implementation UIView (CustomUpdater)
 
 
 - (void)jdb_setBackgroundColor:(NSArray *)colors
 {
     [self jdbappearance_bindUpdater:^(JDBAppearance * _Nonnull appearance, UIView * _Nonnull bindView) {
         bindView.backgroundColor = [UIColor jdbappearance_colorWithHex:colors];
     } updaterKey:@"jdb_setBackgroundColor"];
 }


@end

注意绑定Block的时候须要指定一个updaterKey,updaterKey容许一个UIView绑定多个Block。使用方式也很简单,而且不须要考虑循环引用的问题:

[cell jdb_setBackgroundColor:@[@"#FFFFFF", @"#1D1B1B"]];

App内切换暗黑模式

这个功能容许用户在 App 内手动开启或者关闭暗黑模式,可是存在一个问题:

若是系统开启了暗黑,可是 App 内关闭了,此时一些系统控件的颜色仍然是深色的(例如经过UIImagePickerController调起的系统相册),从而致使系统控件颜色和 App 颜色不一致。

在阐述解决方案以前,先来介绍一下UITraitCollection:

UITraitCollection是 iOS 8 开始新增的一个类,管理着 App 中的用户界面相关的一些系统特征,每一个视图都拥有本身的UITraitCollection。

iOS 13 颜色模式相关的信息,就存储在userInterfaceStyle属性中。若是咱们想给视图单独指定userInterfaceStyle,须要使用 iOS 13 新增的 API overrideUserInterfaceStyle,另外设置overrideUserInterfaceStyle是对子视图生效的。

但是这么多视图,咱们应该修改谁的属性呢?下面这张图描述了视图之间的层级关系以及UITraitCollection的传递路线:

Alt

UITraitCollection是自上而下传递的,可是 UIScreen 和 UIWindowScene 并未提供 overrideUserInterfaceStyle 这个API,咱们只能修改UIWindow的属性,使UIWindow及其全部子视图展现咱们设置的颜色:

  • 若是开启了暗黑,将全部window的overrideUserInterfaceStyle设置为
    UIUserInterfaceStyleDark。
  • 若是关闭了暗黑,将全部window的overrideUserInterfaceStyle设置为
    UIUserInterfaceStyleLight。

若是在 overrideUserInterfaceStyle 修改后,又有新的 window 出现,这种状况要怎么处理呢?咱们注册了UIWindowDidBecomeVisibleNotification通知,这个通知会在一个 UIWindow 对象变为可见的时候发出,在接收到通知后,设置这个window的overrideUserInterfaceStyle属性。

总结:经过修改window的overrideUserInterfaceStyle属性,大多数系统控件的颜色都能和App的颜色保持一致。

监听系统模式切换

为何要提这个呢?用traitCollectionDidChange监听不就能够了吗?

由于咱们发现,在修改overrideUserInterfaceStyle后,当切换系统颜色模式时,window及其子视图的traitCollectionDidChange并无被调用。

虽然官方文档中并无找到明确的说明,可是通过验证,只要咱们将window的 overrideUserInterfaceStyle设置为UIUserInterfaceStyleDark 或 UIUserInterfaceStyleLight,window 及其子视图咱们都无法监听。只有默认的UIUserInterfaceStyleUnspecified才会生效。

那怎么办呢?咱们刚刚把全部window的 overrideUserInterfaceStyle都改了😂😂😂

办法总比困难多!仔细来分析一下,咱们修改window的overrideUserInterfaceStyle是为了同步修改系统控件的颜色。那咱们是否是能够建立一个独立的ObserveWindow,在切换模式的时候,若是是ObserveWindow就跳过,只修改其余window的overrideUserInterfaceStyle。这样就能够在ObserveWindow中实现traitCollectionDidChange方法,处理监听系统模式切换以及更新 App UI 的逻辑:

@implementatiton ObserveWindow
 
 
 - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 {
     if (@available(iOS 13.0, *)) {
         if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
             // 1. 修改 App 内部样式
             // 2. 修改其余 window 的 overrideUserInterfaceStyle
            // 3. 通知业务更新 UI
        }
    }
}


@end

多任务界面快照

在适配过程当中,咱们发现一个问题:在多任务界面,会出现 App 展现的颜色和系统颜色模式恰好相反。

进一步分析后,发现 App 在进入后台时,traitCollectionDidChange 执行了2次,这两次执行过程当中系统的 userInterfaceStyle 分别是 UIUserInterfaceStyleDark 和 UIUserInterfaceStyleLight。

这是为何呢?咱们查看了下traitCollectionDidChange被调用时的堆栈:

Alt

看了堆栈就明白了,系统在进入后台时会建立快照,这个快照其实就是系统多任务界面展现的快照,调用2次是为了分别对深色和浅色进行快照,当进入多任务界面时,系统会根据当前的颜色模式展现正确的快照。

Alt

为何咱们会遇到颜色模式相反的问题呢,这里要先介绍一下“跟随系统”的功能:

App 中有一个开关,用来控制是否跟随系统颜色模式。当用户首次选择切换到暗黑模式,会默认开启跟随系统,此时 App 模式会和系统模式保持一致。若是关闭“跟随系统”的开关,则再也不监听系统模式的切换,以 App 内用户选择的模式为准。

当关闭“跟随系统”的开关后,App 内的颜色模式有可能和系统的不一致,当出现不一致的时候,快照就会出错,好比Dark模式截取了Light模式的图。为了不这种错误,咱们加了一个判断条件,只有“跟随系统”开启的状况下才会开启快照功能。

修改后的traitCollectionDidChange实现以下:

-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 {
     if (@available(iOS 13.0, *)) {
   UIApplicationState state = [UIApplication sharedApplication].applicationState;
         if (state == UIApplicationStateBackground) {
             // 系统切换到后台时,会对颜色模式取反截2张图
             JDBAppearanceManager *manager = [JDBAppearanceManager sharedInstance];
             if (manager.followSystemMode) {
                 // 若是跟随系统,就更新UI,系统会在UI更新完成后进行快照
            }
        } else {
            // 触发场景:系统控制中心切换模式、后台进入前台、Xcode调试菜单切换模式
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
                // 1. 修改 App 内部样式
                // 2. 修改其余 window 的 overrideUserInterfaceStyle
                // 3. 通知业务更新 UI
            }
        }
    }
}

个性化定制

基础组件的定位,除了为京东 App 的暗黑模式适配提供支持,咱们还但愿能够给更多的 App 使用。暗黑基础组件在支持现有功能的基础上,也支持个性化定制功能或者API,接入方能够根据本身的需求灵活选择:

  • App 内部切换开关
  • 多任务快照
  • 自定义 Updater
  • 自定义颜色模式

### 但愿你们不要重复采坑

本文详细介绍了京东 App iOS 暗黑模式适配过程当中踩过的坑,以及整个方案的实现原理,但愿对你们有所帮助。

欢迎点击“京东智联云”了解更多精彩内容!

Alt

Alt

相关文章
相关标签/搜索