今天的 WWDC 19 上发布了 iOS 13,我们来看下如何适配 DarkMode
首先我们来看下效果图
如何适配 DarkMode
DarkMode 主要从两个方面来适配,一是颜色,二是图片,适配的代码不是很多,接下来让我们一起来看看具体是怎么操作的吧。
颜色适配
iOS 13 之前 UIColor
只能表示一种颜色,从 iOS 13 开始 UIColor
是一个动态的颜色,它可以在 LightMode 和 DarkMode 拥有不同的颜色。
iOS 13 下 UIColor
增加了很多动态颜色,我们来看下用系统提供的颜色能实现怎么样的效果。
// UIColor 增加的颜色
@available(iOS 13.0, *)
open class var systemBackground: UIColor { get }
@available(iOS 13.0, *)
open class var label: UIColor { get }
@available(iOS 13.0, *)
open class var placeholderText: UIColor { get }
...
view.backgroundColor = UIColor.systemBackground
label.textColor = UIColor.label
placeholderLabel.textColor = UIColor.placeholderText
怎么样,看起来和 iOS 13 之前设置一个颜色的方法一样吧,用这种动态颜色,系统直接替我们完成了适配的工作,是不是很方便呢。
如何自己创建一个动态的 UIColor
上面我们说到系统提供了一些动态的颜色供我们使用,但是在正常开发中,系统提供的颜色肯定是不够用的,所以我们要自己创建动态颜色。
iOS 13 下 UIColor
增加了一个初始化方法,我们可以用这个初始化方法来创建动态颜色。
@available(iOS 13.0, *)
public init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)
这个方法要求传一个闭包进去,当系统从 LightMode 和 DarkMode 之间切换的时候就会触发这个回调。
这个闭包返回一个 UITraitCollection
类,我们要用这个类的 userInterfaceStyle
属性。
userInterfaceStyle
是一个枚举,声明如下
@available(iOS 12.0, *)
public enum UIUserInterfaceStyle : Int {
case unspecified
case light
case dark
}
这个枚举会告诉我们当前是 LightMode or DarkMode
现在我们创建两个 UIColor
并赋值给 view.backgroundColor
和 label
,代码如下
let backgroundColor = UIColor { (trainCollection) -> UIColor in
if trainCollection.userInterfaceStyle == .dark {
return UIColor.black
} else {
return UIColor.white
}
}
view.backgroundColor = backgroundColor
let labelColor = UIColor { (trainCollection) -> UIColor in
if trainCollection.userInterfaceStyle == .dark {
return UIColor.white
} else {
return UIColor.black
}
}
label.textColor = labelColor
现在,我们做完了动图中背景色和文本颜色的适配,接下来我们看看图片如何适配
图片适配
打开 Assets.xcassets
把图片拖拽进去,我们可以看到这样的页面
然后我们在右侧工具栏中点击最后一栏,点击 Appearances
选择 Any, Dark
,如图所示
我们把 DarkMode 的图片拖进去,如图所示
最后我们加上 ImageView
的代码
imageView.image = UIImage(named: "icon")
现在我们就已经完成颜色和图片的 DarkMode 适配,是不是很简单呢 (手动滑稽)
如何获取当前模式 (Light or Dark)
我们可以看到,不管是颜色还是图片,适配都是系统完成的,我们不用关心现在是什么样的样式。
但是在某些场景下,我们可能会有根据当前样式来做一些其他适配的需求,这时我们就需要知道现在什么样式。
我们可以在 UIViewController
或 UIView
中调用 traitCollection.userInterfaceStyle
来获取当前视图的样式,代码如下
if trainCollection.userInterfaceStyle == .dark {
// Dark
} else {
// Light
}
那么我们什么时候需要用这样的方法做适配呢,比如说当我们使用 CGColor
的时候,上面说到 UIColor
在 iOS 13 下变成了一个动态颜色,但是 CGColor
仍然只能表示单一的颜色,所以当我们使用到 CGColor
的时候,我们就可以用上面的方法做适配。
颜色
对于 CGColor
我们还有还有另一种适配方法,代码如下
let resolvedColor = labelColor.resolvedColor(with: traitCollection)
layer.borderColor = resolvedColor.cgColor
resolvedColor
方法会根据传递进去的 traitCollection
返回对应的颜色。
图片
对于 UIImage
我们也有类似的方法,代码如下
let image = UIImage(named: "icon")
let resovledImage = image?.imageAsset?.image(with: traitCollection)
如何监听模式变化
上面我们说了如何获取当前模式,但是我们要搭配监听方法一起使用,当 light dark 模式切换的时候,要把上面的代码再执行一遍。系统为我们提供了一个回调方法,当 light dark 切换时就会触发这个方法。
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
// 适配代码
}
}
题外话
如果你觉得这样为 CGColor
做适配很麻烦,那么不妨试试 XYColor 这个框架。
如何改变当前模式
我们可以看到在动图中是直接改系统的模式,从而让 App 的模式修改,但是对于某些有夜间模式功能的 App 来说,如果用户打开了夜间模式,那么即使现在系统是 light 模式,也要强制用 dark 模式。
我们可以用以下代码将当前 UIViewController
或 UIView
的模式。
overrideUserInterfaceStyle = .dark
print(traitCollection.userInterfaceStyle) // dark
我们可以看到设置了 overrideUserInterfaceStyle
之后,traitCollection.userInterfaceStyle
就是我们设置后的模式了。
需要给每一个 Controller 和 View 都设置一遍吗
答案是不需要,我们先来看一张图。
当我们设置一个 controller 为 dark 之后,这个 controller 下的 view,都会是 dark mode,但是后续 present 的 controller 仍然是跟随系统的样式。
因为苹果对 overrideUserInterfaceStyle
属性的解释是这样的。
当我们在一个普通的 controlle, view 上重写这个属性,只会影响当前的视图,不会影响前面的 controller 和后续 present 的 controller。
但是当我们在 window
上设置 overrideUserInterfaceStyle
的时候,就会影响 window
下所有的 controller, view,包括后续推出的 controller。
但是当我们在 感谢 hostname 指出错误window.rootViewController
上设置 overrideUserInterfaceStyle
的时候,就会影响 rootViewController
下所有的 controller, view,包括后续推出的 controller。
我们回到刚刚的问题上,如果 App 打开夜间模式,那么很简单我们只需要设置 window
的 overrideUserInterfaceStyle
属性就好了。
题外话:当我们用 Xcode11 创建项目,我们会发现项目结构发生了变化,window
从 AppDelegate
移到 SceneDelegate
中。
那么如何获取 SceneDelegate
中的 window
呢,代码如下
// 这里就简单介绍一下,实际项目中,如果是iOS应用这么写没问题,但是对于iPadOS应用还需要判断scene的状态是否激活
let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .dark
其他内容
Status Bar
之前 Status Bar
有两种状态,default
和 lightContent
现在 Status Bar
有三种状态,default
, darkContent
和 lightContent
现在的 darkContent
对应之前的 default
,现在的 default
会根据情况自动选择 darkContent
和 lightContent
UIActivityIndicatorView
之前的 UIActivityIndicatorView
有三种 style
分别为 whiteLarge
, white
和 gray
,现在全部废弃。
增加两种 style
分别为 medium
和 large
,指示器颜色用 color
属性修改。
如何在模式切换时打印日志
在 Arguments
中的 Arguments Passed On Launch
里面添加下面这行命令。
-UITraitCollectionChangeLoggingEnabled YES
以上是 iOS 13 如何适配 Dark Mode 的全部内容,如有错误欢迎指出。
WWDC链接 Implementing Dark Mode on iOS
如果你想知道 iOS 13 还增加了什么新特性可以阅读这篇文章。