WWDC2019之 Dark Mode相关内容

自从OS X引入了 dark mode后, iOS13也引入了该模式,接下来就看下如何适配 dark mode。

原理

结合WWDC2019-214的视频,可以知道 mode 切换的捕捉和 UITraitCollection 密切相关。 而UITraitCollection 是从 Screen开始逐步影响到View:

UIScreen->UIWindowScene->UIWindow->UIPresentationController->UIViewController->UIView Hierarchy

在这个树形结构中,下级元素的 traitCollection 会“继承”上级的。
接下来,我们看下WWDC2019-214中的一张表格:

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

第一、二行是 traitCollection会被设置的地方;第三行顾名思义是 traitCollection 变化后触发的方法。由此,我们可以明白何时需要根据traitCollection设置初始化的颜色以及发生变化时如何正确地接收响应。
另外,对于UIViewController/UIView,可以设置属性 overrideUserInterfaceStyle,设置了以后当前ViewController/View进进入指定的模式,而不受上级元素的影响——这主要用于调试。如果想对整个App设置某个模式,需要在 info.plist 文件中添加属性: UIUserInterfaceStyle, 取值 LightDark

明白了以上机制后,我们自然地产生问题:

  1. 新增了哪些用于mode的API
  2. 如何根据traitCollection的变化, 正确地初始化以及更新控件
  3. App设计上由此需要注意的地方

iOS13 中的API变化

dark mode 的引入主要影响的是颜色,因此对控件的影响主要是颜色:背景色,字体颜色,还有图片,indicator等控件。
在iOS13中,系统引入了 dynamic color 来实现在不同模式下显示不同颜色的目的,并且系统定义了一系列主题颜色, 如: UIColor.systemBackground。 当然,预先设置的主题色未必能满足每个 App,自定义 dynamic color也很简单:

// custom dynamic cokors
let dynamicColor = UIColor { (traitCollection: UITraitCollection) -> UIColor in 
    if traitCollection.userInterfaceStyle == .dark {
        return .black 
    } else {
        return .white 
    }
}

对于图片,iOS13支持在不同的模式下显示不同的内容,有两种实现途径。
第一种,使用asset资源。在资源界面右下角找到 Appearence,然后对应不同的模式添加图片,使用方式不变。
第二种方式,就是根据当前的 traitCollection(或变化),修改图片,这个放在下一节说明。

获取dynamic color中的真实颜色

可以通过如下方式获取当前状态下的真实颜色:

let dynamicColor = UIColor.systemBackground
let traitCollection = view.traitCollection
let resolvedColor = dynamicColor.resolvedColor(with: traitCollection)

同样的,也可以获取当前状态下的真实图片:

let image = UIImage(named: "HeaderImage")
let asset = image?.imageAsset
let resolvedImage = asset?.image(with: traitCollection)

materials 设计

blur 和 vibrancy effect 都增加了新的 API(4种style) 以支持模式切换。

其它变化

UIActivityIndicatorView 的式样有了更新,且需要自己设置 color。
NSAttributedString 中的属性,最好添加上颜色属性,并且赋值 dynamic color。

响应变化 when dynamic color might change

根据前文的描述,我们明白只需要在 traitCollectionDidChange 中正确地处理即可。如果在初始化的时候,使用了 dynamic color/image时,那么该方法可以忽略不管。

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) 
{ 
    super.traitCollectionDidChange(previousTraitCollection)
    if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { 
        // Resolve dynamic colors again
    } 
}

CGColor

CGColor需要单独说明是因为CGColor没有类似UIColor中的 dynamic color。因此需要手动设置,有三种方法:

let layer = CALayer()
let traitCollection = view.traitCollection
// Option 1
let resolvedColor = UIColor.label.resolvedColor(with: traitCollection)
layer.borderColor = resolvedColor.cgColor

// Option 2
traitCollection.performAsCurrent {
layer.borderColor = UIColor.label.cgColor
}

// Option 3
let savedTraitCollection = UITraitCollection.current
UITraitCollection.current = traitCollection
layer.borderColor = UIColor.label.cgColor 
UITraitCollection.current = savedTraitCollection

总结

  1. 使用 dynamic color/image
  2. 在traitCollect设置和变化的地方,正确地响应
  3. CGColor/UIActivityIndicatorView/NSAttributedString 的不同
  4. materials设计中使用相应地新API
  5. 调试:添加属性和环境变量