Skip to content

使用 QMUITheme 实现换肤并适配 iOS 13 Dark Mode

MoLice edited this page Jul 23, 2019 · 8 revisions

背景

一、iOS 13 Dark Mode

iOS 13 系统新增了 Dark Mode,需要项目自行适配,系统提供的适配方案简述如下:

  1. UIView 层面
    1. 对于 UIColor,使用 UIColor (DynamicColors) 里提供的新 API 去初始化颜色,例如 +[UIColor colorWithDynamicProvider:],或者用 Xcode 11 在 Assets 里为每个颜色创建一个 Color Set 然后在 Appearances 里选择带有 Dark 的选项来生成一个动态颜色,代码里通过 +[UIColor colorNamed:] 来获取颜色。
    2. 对于 UIImage,使用 Xcode 11 在 Assets 里的 Appearances 选择带有 Dark 的选项来创建一个动态的图片。
    3. 对于 UIVisualEffect,使用 iOS 10 之后的 UIBlurEffectStyle(例如 UIBlurEffectStyleRegular),系统会自动切换 Light/Dark 样式。
    4. 其他内容可在 traitCollectionDidChange: 里根据 self.traitCollection.userInterfaceStyle 的值来判断当前的主题。
  2. UIViewController 层面,通过在 traitCollectionDidChange: 里根据 self.traitCollection.userInterfaceStyle 的值来判断当前的主题。

二、系统适配方案的局限性

  1. 对于 UIColor,系统有 API 生成动态颜色,但 CGColor 却没有,所以使用到 CGColor 的地方都需要重写 traitCollectionDidChange: 在里面重新设置一遍,无法保持代码风格的一致性。

  2. 在 App 回到后台的时候,系统会分别使用 Light Mode 和 Dark Mode 为 App 当前界面截两张图,从而保证你切换了主题后再唤醒 App,不会看到颜色跳变的过程,保证了体验,但由于 CALayer 带有隐式动画,在截图时 UIView 已经渲染完新样式了,CALayer 还是旧的,从而表现出明显的 UI 问题(注意下图最终唤醒 App 后,色块(CALayer)能看到从白变黑的过程,而界面上其他地方的颜色是看不到变化过程的)。

  3. 对于 UIColorUIImage 这些对象,如果你将其用于 UIView.backgroundColorUIView.tintColor 这种系统原生自带的属性,当设备主题发生切换时,UIView 会自动去刷新这些值(因为它知道有这些值需要刷新),但如果你是一个自定义 View 里的自定义属性(例如 QMUIGridView.separatorColor),系统并不知道你总共有哪些属性需要更新,所以你依然需要借助 traitCollectionDidChange: 来重新设置一遍,导致 UI 代码需要分散到多个地方,不好维护。

  4. 从开发者的角度,让一个项目兼容 iOS 13 Dark Mode,和让项目在所有 iOS 版本下都能支持 Dark Mode,这两者的工作量相差不大,都是要对已有的 UI 代码做大量修改。如果要兼容 iOS 13 Dark Mode,最大的收益肯定是同时支持所有 iOS 版本,然而系统提供的方案并不考虑 iOS 12 及以下版本的实现。

三、QMUI 提供的方案

基于以上状况,我们设计了 QMUITheme 组件,它解决的问题是:

  1. 支持全 iOS 版本的换肤,可设置多个主题。

  2. 兼容 iOS 13 Dark Mode,可将 Dark Mode 映射为 App 中的某一个主题,对业务而言只需要关心业务主题,不需要关心设备当前是否开启了 iOS 13 Dark Mode。

  3. 支持 UIColorCGColorUIImageUIVisualEffect 的动态化,在 View 初始化的时候直接使用这些动态对象即可,不需要强制写在某些 updateXxx、xxxDidChange 的方法里,以保持最优雅的编码风格。

  4. 支持 UIKit 自带的 View 组件和业务自定义 View 组件里与颜色、图片相关属性的自动刷新。

QMUITheme 使用教程

下面以 QMUI Demo 为例,讲解 QMUITheme 组件的使用。QMUI Demo 拥有5个主题,其中“Dark”主题对应 iOS 13 Dark Mode。在 iOS 13 下,每次 QMUI Demo 启动时都会根据当前设备是否开启了 Dark Mode,来强制将 QMUI Demo 的主题设置为“Default” 或 “Dark”。

一、基本概念解释

  1. QMUIThemeManager

    这是全局的主题管理器,可以注册、移除主题,切换当前主题,在 iOS 13 下负责监听系统 Dark Mode 的切换,并将其映射为已注册的某个业务主题。

  2. 主题(theme)

    对项目而言,主题可以是任意的对象类型,存在多个主题时,不同主题也可以是不相同的类型,甚至可以是一个无意义仅占位用的 NSNull。一个主题对象通常用于存储全局的颜色、图片等信息。在 QMUI Demo 里,每个主题对应一个 QMUI 配置表,也即 NSObject<QDThemeProtocol> * 类型。

    QMUIThemeManager 而言,一个主题对应一个唯一的 identifier。与主题类似,identifier 对类型也没有要求,只需要支持 NSCopying 协议即可,所以可以用 NSStringNSNumber等常见类型,怎么方便怎么来。

  3. 动态对象 换肤的直接操作对象通常只有 UIColor(包含 CGColor)、UIImageUIVisualEffect 这三种,在以前,当你拿到一个 UIColor 对象,它对应什么实体的色值,是确定的,但在 iOS 13 或 QMUITheme 体系下,同一个 color 对象在不同的主题下可能会展示出不同的色值,于是这种对象我们称之为“动态对象”。动态对象均需要以特定的方式来创建,例如: