32b7444b732454aced188004744ab974
Session 214:如何在 iOS 13 上适配深色模式?

一分钟 Session 速读

iOS 上对深色模式的适配方式和 macOS 上的深色模式的适配方式比较类似,此次 Session 中提到的主要内容有:

  • 首先,所有 UIKit 本身所提供的 UI 控件(例如 Tabbar) ,只要没有针对颜色等内容特殊设置过,都会自动适配深色模式,这部分是开发者无需关心的
  • 开发者可以通过 UIKit 在 UI 控件的颜色、模糊效果、图片这三个方面新提供的 API,来让自己的 App 适配深色模式,具体方式为:
    • 给 UI 控件设置颜色的时候,不要设置类似 UIColor.black 这样的绝对值颜色,而是设置 UIKit 中新提供的动态颜色(Dynamic Colors),比如 UIColor.systemBackground
    • 利用 UIVisualEffectView 来创建一些类似模糊的效果时,不要设置类似 UIBlurEffect.UIBlurEffectStyleExtraLight 这样带有明确颜色的效果,而是设置 UIKit 中新提供的动态样式的效果,比如 UIBlurEffect.systemThinMaterial
    • 利用 xcassets 管理图片和颜色的时候,如果有必要,开发者可以使用 xcassets 在 Xcode 11 中新增的功能,为深色模式额外指定一个图片或者颜色
  • 针对那些自定义的 UI 控件,开发者可以通过使用 UITraitCollection ,通过判断当前系统的颜色模式来完成对深色模式的适配

Session 中提到的内容只是一个关于深色模式适配的一个基础内容,接下来,我会结合 Session 中的内容,对如何适配深色模式做一个展开的讲解。

如何在 iOS 上适配深色模式?

在适配深色模式的过程中,作为开发者,我们只要能够解决以下这两个问题,基本就可以完成对深色模式的适配了:

  1. 在 iOS 13 中,我们如何判断当前系统的颜色模式?
  2. 在 iOS 13 中,我们应该对哪些 UI 上的内容适配深色模式?

我们来分别看看这两个问题应该如何解决。

如何判断当前系统的颜色模式?

在 iOS 13 中,我们可以通过 UITraitCollection 来判断当前系统的颜色模式。在 iOS 系统中,UITraitCollection 已经成为一个管理所有用户界面相关信息的大管家了:

The iOS interface environment for your app, defined by traits such as horizontal and vertical size class, display scale, and user interface idiom.

而颜色模式的相关信息,被存放在它的 userInterfaceStyle 属性中。

在 iOS 中,我们所熟悉的 UIView 和 UIViewController 、UIScreen、UIWindow 都已经遵从了 UITraitEnvironment 这个协议,因此这些类都拥有一个叫做 traitCollection 的属性,在这些类的方法中,我们可以这样去判断当前 App 的颜色模式:

let isDark = traitCollection.userInterfaceStyle == .dark

除此之外,我们还可以使用 UITraitCollection.current 这个类属性来获取当前 App 的颜色模式。不过需要注意的是在,并不是在所有的地方使用这个 API 都是正确的,只有在下面这些方法中,才可以放心的使用这个 API:

UIView UIViewController UIPresentationController
draw()
layoutSubview() viewWillLayoutSubviews()
viewDidLayoutSubviews()
containerViewWillLayoutSubviews()
containerViewDidLayoutSubviews()
traitCollectionDidChange()
tintColorDidChange()
traitCollectionDidChange() traitCollectionDidChange()

关于 UITraintCollection 的更多细节,我们会在后面的小节中展开讲述,现在,先让我们继续看看我们的第二个问题。

哪些内容是我们应该适配的?

在深色模式的适配过程中,我们主要应该关心的,是这三个方面的代码:

  1. 颜色
  2. 模糊效果
  3. 图片

原因在于,这三个方面的代码中会比较容易写出来和颜色强绑定的内容。我们来一个个看看这三个方面的代码应该如何去修改。

颜色

关于颜色,我们在适配的过程中需要关心的,是那些我们写死颜色色值的代码,例如下面的这种代码:

override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        self.textView.textColor = .init(red: 0, green: 1, blue: 2, alpha: 1)
        self.textView.backgroundColor = UIColor(named: "textViewBackgroundColor")!
}

我们首先来看看类似下面这行的代码应该如何修改:

self.view.backgroundColor = .white

在 iOS 13 中,UIKit 为我们新提供了很多预设的"动态颜色",这些颜色的使用方式和一般的颜色并无二致。当我们给 UI 控件设置了动态颜色以后,UI 控件就会自动的根据当前是否是黑暗模式展现出来对应的颜色。例如,如果我们将 view 的背景色设置为 systemBackground,那么在深色模式下,view 的背景色就会是黑色,而在普通情况下就会是白色:

这里还有一个小细节,就是同样设置了 systemBackground 的 View,处于不同的视图层级时会有不同的颜色表现:

这个视图层级的信息保存在 traitCollection.userInterfaceLevel 这个属性中,这个特性的目的,是为了解决在深色模式中,不同层级的视图无法通过阴影来明确区分的问题:

来自作者的小提示

iOS 13 中新增了很多这样的预设的动态颜色,完整的颜色列表可以参见(所有标记为 beta 的颜色都是此次新增的动态颜色):

如果想知道这些颜色在普通模式和深色模式下的表现,可以在 Apple Design Resources 中下载 iOS 对应的设计文件。为了方便大家查看,我将其中和深色模式相关的部分导出为 PDF,存放在 这个仓库中

因此,上面的第一部分的代码,我们可以这样来完成适配:

self.view.backgroundColor = .systemBackground

不过,系统提供的这些颜色在实际的开发过程中一定是无法完全符合我们的需求的,因此在实际开发中,我们也可能创建很多我们自定义的动态颜色。在 iOS 13 中,我们可以使用 UIKit 为 UIColor 所提供的 新 API 来创建我们自己的动态颜色,方法很简单,只要在创建 UIColor 的时候传入一个闭包,在闭包中根据 UITraitCollection 返回对应的真实颜色即可:

extension UIColor {
    class var myDynamicColor: UIColor {
        get {
            .init(dynamicProvider: { (traitCollection) -> UIColor in
                if traitCollection.userInterfaceStyle == .dark {
                    return UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 1)
                } else {
                    return UIColor(displayP3Red: 255, green: 255, blue: 255, alpha: 1)
                }
            })
        }
    }
}

因此,我们上面的第二部分的代码,就可以修改成:

self.textView.textColor = .myDynamicColor

而针对从 xcassets 中读取的颜色,在 iOS 13 中,我们可以通过 Xcode 11 的新功能,为 xcassets 中的颜色额外设置深色模式时的实际颜色:

因此,对于上面的第三种代码,我们并不需要做任何代码上的变更,直接修改 xcassets 文件就可以完成对深色模式的适配。

模糊效果

除了颜色,在 iOS 系统中,我们创建模糊效果的 UIVisualEffectView 的代码也会有硬编码的情况,例如:

let blurView = UIVisualEffectView(effect: UIBlurEffect.init(style: .light))

在 iOS 13 之前,我们能够使用的 UIBlurEffect 的样式有:

这些样式由于都包含明显的 dark 或者 light 属性,因此直接使用它们在某些场景下可能就会显得格格不入。在 iOS 13 中,我们也应该让这些模糊效果随着系统模式的切换而随之切换:

来自作者的小提示

实际上在 iOS 10 之后,我们又有两种样式可以使用:

不过根据 What's New in tvOS - WWDC 2016 来看,当时增加这两种样式只是为了让开发者能够更好的适配 tvOS 新增的深色模式,讲道理这两个样式应该是 tvOS only 的,但不确定为什么苹果最终还是让他能够在 iOS 系统上使用,因此在 iOS 系统上我们一般也不用关心这两个样式。在 tvOS 上,使用 regular 会根据当前系统的模式分别展示为 lightdark,而使用 prominent 则会分别展示为 extraLightextraDark。这两个属性的使用场景是一个容易让人困惑的点,这里特别感谢在 iOS UI 上有深厚造诣的 SketchK 同学提供的帮助。

在苹果最新的 HIG 规范中,这些模糊样式也被称作 Material,而随着 HIG 的更新,UIKIt 也为我们新提供了四种模糊样式:

这四种样式是动态的模糊效果,这也就意味着,如果我们使用了这些样式,我们的模糊效果就会自动的匹配当前系统的模式:

需要注意的是,在 iOS 13 中,这些 Material 的样式也有着对应的永远为 Light 或者 Dark 的版本:

top Created with Sketch.