如下文章来源于京东零售技术,做者平台研发姚琦app
iOS 13 苹果推出了暗黑模式,暗黑模式在夜间能够更好的保护视力,也能够节省 App 电量消耗。可是 Apple 提供的暗黑模式只支持 iOS 13,为了给用户带来更好的体验,咱们但愿 iOS 13 如下的系统也能够支持暗黑模式。另外咱们还给用户提供了自主选择的权利,能够在 App 内手动关闭暗黑模式,不跟随系统主题变化。异步
京东 App 涉及业务模块众多,整个适配工做量巨大,为了解决上述问题,并让各模块经过统一的接口快速接入,咱们开发了暗黑基础组件,提供如下能力:ide
基础组件设计方案以下:工具
业务接入spa
业务接入时须要调用基础组件提供的jdbappearance_bindUpdater方法,传入一个Block并在其中处理UI更新的逻辑,基础组件会绑定Block和UIView,而后将UIView存储在HashTable中,在合适的时机经过遍历HashTable和执行绑定的Block来更新UI。业务组件的接入方案以下:设计
须要注意的是,遍历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 内关闭了,此时一些系统控件的颜色仍然是深色的(例如经过UIImagePickerController调起的系统相册),从而致使系统控件颜色和 App 颜色不一致。
在阐述解决方案以前,先来介绍一下UITraitCollection:
UITraitCollection是 iOS 8 开始新增的一个类,管理着 App 中的用户界面相关的一些系统特征,每一个视图都拥有本身的UITraitCollection。
iOS 13 颜色模式相关的信息,就存储在userInterfaceStyle属性中。若是咱们想给视图单独指定userInterfaceStyle,须要使用 iOS 13 新增的 API overrideUserInterfaceStyle,另外设置overrideUserInterfaceStyle是对子视图生效的。
但是这么多视图,咱们应该修改谁的属性呢?下面这张图描述了视图之间的层级关系以及UITraitCollection的传递路线:
UITraitCollection是自上而下传递的,可是 UIScreen 和 UIWindowScene 并未提供 overrideUserInterfaceStyle 这个API,咱们只能修改UIWindow的属性,使UIWindow及其全部子视图展现咱们设置的颜色:
若是在 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被调用时的堆栈:
看了堆栈就明白了,系统在进入后台时会建立快照,这个快照其实就是系统多任务界面展现的快照,调用2次是为了分别对深色和浅色进行快照,当进入多任务界面时,系统会根据当前的颜色模式展现正确的快照。
为何咱们会遇到颜色模式相反的问题呢,这里要先介绍一下“跟随系统”的功能:
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 iOS 暗黑模式适配过程当中踩过的坑,以及整个方案的实现原理,但愿对你们有所帮助。
欢迎点击“京东智联云”了解更多精彩内容!