Skip to content

14功能组件:如何使用语义色,支持深色模式?

从 iOS 13 开始,用户可以从系统级别来把外观模式改成深色模式(Dark mode)。与原有的浅色模式(Light mode)相比,使用深色模式具有以下几大优点:

  1. 由于减少发光,使用深色模式能大幅减少电量的消耗,延长 iPhone 的续航能力;

  2. 对视力不佳或者与对强光敏感的用户更为友好,为他们提供更好的可视性;

  3. 在暗光环境下,让用户使用手机时更舒服。

那么,我们的 App 怎样才能在支持深色模式呢?下面我将结合咱们的项目案例 Moments App 来介绍下。

iOS 语义色

对于深色模式的支持,苹果推荐使用语义化颜色 (Semantic colors)来进行适配。什么叫语义化颜色呢?语义化颜色是我们根据用途来定义颜色的名称,例如使用在背景上的颜色定义为background,主文本和副文本的颜色分别定义为primaryTextsecondaryText。UI 可以通过语义色来灵活地适配用户所选择的外观模式,比如背景在浅色模式下显示为白色,而在深色模式下显示为黑色。

为了简化深色模式的适配过程,苹果公司提供了具有语义的系统色 (System colors)和动态系统色(Dynamic system colors)供我们使用。

iOS 系统色 (来源:developer.apple.com)

上图是苹果开发者网站提供的一个 iOS 系统色,有蓝色、绿色、靛蓝、橙色、黄色等,它们在浅色模式和深色模式下会使用到不同的颜色值。比如蓝色,在浅色模式下,它的 RGB 分别是 0、122、255,在深色模式下则分别为 10、132、255。这样就能保证系统蓝色在不同的外观模式的背景颜色上都能清晰显示。

iOS 动态系统色 (来源:developer.apple.com)

上图显示是 iOS 系统提供的动态系统色的定义。它们都是通过用途来定义各种颜色的名称。例如 Label 用于主标签文字的颜色,而 Secondary label用于副标签文字的颜色,使用它们就能自动支持不同的外观模式了。

Moments App 的语义色

为了增强品牌效果,我们一般都会为 App 单独定义一组语义色。下面以 Moments App 为例看看如何在代码中定义语义色。

根据 07 讲的设计规范,我们在 DesignKit 组件里面自定义了一组语义色,具体代码如下:

swift
public extension UIColor {
    static let designKit = DesignKitPalette.self
    enum DesignKitPalette {
        public static let primary: UIColor = dynamicColor(light: UIColor(hex: 0x0770e3), dark: UIColor(hex: 0x6d9feb))
        public static let background: UIColor = dynamicColor(light: .white, dark: .black)
        public static let secondaryBackground: UIColor = dynamicColor(light: UIColor(hex: 0xf1f2f8), dark: UIColor(hex: 0x1D1B20))
        public static let tertiaryBackground: UIColor = dynamicColor(light: .white, dark: UIColor(hex: 0x2C2C2E))
        public static let line: UIColor = dynamicColor(light: UIColor(hex: 0xcdcdd7), dark: UIColor(hex: 0x48484A))
        public static let primaryText: UIColor = dynamicColor(light: UIColor(hex: 0x111236), dark: .white)
        public static let secondaryText: UIColor = dynamicColor(light: UIColor(hex: 0x68697f), dark: UIColor(hex: 0x8E8E93))
        public static let tertiaryText: UIColor = dynamicColor(light: UIColor(hex: 0x8f90a0), dark: UIColor(hex: 0x8E8E93))
        public static let quaternaryText: UIColor = dynamicColor(light: UIColor(hex: 0xb2b2bf), dark: UIColor(hex: 0x8E8E93))
        static private func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
            return UIColor { $0.userInterfaceStyle == .dark ? dark : light }
        }
    }
}
public extension UIColor {
    convenience init(hex: Int) {
        let components = (
                R: CGFloat((hex >> 16) & 0xff) / 255,
                G: CGFloat((hex >> 08) & 0xff) / 255,
                B: CGFloat((hex >> 00) & 0xff) / 255
        )
        self.init(red: components.R, green: components.G, blue: components.B, alpha: 1)
    }
}

我们为UIColor定义了一个类型扩展(Extension)。为了调用时具有命名空间,我们在这个扩展里定义了一个名叫DesignKitPalette的内嵌枚举类型(Nested enum),然后定义了一个静态属性来引用该枚举。

首先,我们一起看看DesignKitPalette两个公用的方法。第一个是func dynamicColor(light: UIColor, dark: UIColor) -> UIColor,在该方法里面,我们根据用户当前选择的userInterfaceStyle来返回对应的深色或者浅色。

第二个方法是通过类型扩展来为UIColor类型添加了一个初始化函数(构造函数)。该初始化函数接收一个Int类型的参数,这个参数保存了一个十六进制的值。函数内部从hex里面取出分别表示红色、绿色和蓝色的RGB的值,例如传入的hex0x0770e3,那么RGB的值是分别是0770e3, 然后把这些值传递给原有的init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)初始化函数来生成一个UIColor的实例。

有了这两个函数以后,我们就可以很方便地定义设计规范里面的各种颜色了。具体来说,只需要把浅色和深色传递给语义色的属性就可。比如,我们的语义色primary所对应的浅色和深色的十六进制分别是0x0770e30x6d9feb,那么我们就可以通过这两个值来生成一个支持动态颜色的 UIColor 对象,代码如下所示。

swift
public static let primary: UIColor = dynamicColor(light: UIColor(hex: 0x0770e3), dark: UIColor(hex: 0x6d9feb))

有了这些定义以后,我们可以在代码中很方便地使用它们。代码如下:

swift
label.textColor = UIColor.designKit.primaryText
view.backgroundColor = UIColor.designKit.background

可以看到,我们可以通过UIColor.designKit取出相应的语义色并赋值给类型为UIColor的属性即可。

测试语义色

当我们的 App 使用了语义色以后,要经常在浅色和深色模式之间来回切换,加以测试,及时发现问题解决问题。要不然在开发过程中可能会因为不小心引入影响可读性的 Bug ,从而降低用户体验。幸运的是,iOS 的 Simulator 为我们提供了一组快捷键Command + Shift + A来快速切换外观模式。下面是 Moments App 在不同外观模式下运行的效果。

从视频上你可以看到,当我按下快捷键Command + Shift + A的时候 Moments App 在浅色和深色模式之间自动来回切换。这样能帮我们快速检查界面上文本的可读性。

总结

在这一讲中我介绍了如何通过语义色来灵活支持不同的外观模式,同时以 Moments App 为例子介绍了如何通过UIColor的扩展来自定义语义色。

当我们的 App 使用了语义色以后,还需要注意以下几点。

  1. 不要把深色模式等于黑夜模式或者夜间模式,支持深色模式的 App 在正常光线的环境下也要为用户提供良好的视觉舒适度。

  2. App 应该从系统设置里面读取外观模式的信息,而不是让用户在 App 里面进行单独配置。

  3. 在开发过程中,要经常切换外观模式来测试 App。

  4. 要在设置 App ->辅助功能 ->显示与字体大小 页面中修改降低透明度增强对比度开关,检查深色内容在黑色背景下的可读性。

思考题:

除了上述通过代码的方式以外,我们还可以在资源目录(Asset Catalog)中添加语义色。请问这两种办法各有什么优缺点?

可以把你的想法和答案写到下面的留言区哦,我们下一讲将介绍如何通过 BFF 设计跨平台的系统架构。

源码地址:

定义语义色的文件地址:
https://github.com/lagoueduCol/iOS-linyongjian/blob/main/Frameworks/DesignKit/src/Color/UIColorExtensions.swift