自从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
, 取值 Light
或Dark
。
明白了以上机制后,我们自然地产生问题:
- 新增了哪些用于mode的API
- 如何根据traitCollection的变化, 正确地初始化以及更新控件
- 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
总结
- 使用 dynamic color/image
- 在traitCollect设置和变化的地方,正确地响应
- CGColor/UIActivityIndicatorView/NSAttributedString 的不同
- materials设计中使用相应地新API
- 调试:添加属性和环境变量
Comments