iOS13-适配夜间模式/深色外观(Dark Mode)

18,497

今天的 WWDC 19 上发布了 iOS 13,我们来看下如何适配 DarkMode

首先我们来看下效果图

效果图.gif

如何适配 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.backgroundColorlabel,代码如下

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)

我们可以看到,不管是颜色还是图片,适配都是系统完成的,我们不用关心现在是什么样的样式。

但是在某些场景下,我们可能会有根据当前样式来做一些其他适配的需求,这时我们就需要知道现在什么样式。

我们可以在 UIViewControllerUIView 中调用 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 模式。

我们可以用以下代码将当前 UIViewControllerUIView 的模式。

overrideUserInterfaceStyle = .dark
print(traitCollection.userInterfaceStyle)  // dark

我们可以看到设置了 overrideUserInterfaceStyle 之后,traitCollection.userInterfaceStyle 就是我们设置后的模式了。

需要给每一个 Controller 和 View 都设置一遍吗

答案是不需要,我们先来看一张图。

当我们设置一个 controller 为 dark 之后,这个 controller 下的 view,都会是 dark mode,但是后续推出的 controller 仍然是跟随系统的样式。

因为苹果对 overrideUserInterfaceStyle 属性的解释是这样的。

当我们在一个普通的 controlle, view 上重写这个属性,只会影响当前的视图,不会影响前面的 controller 和后续推出的 controller。

但是当我们在 window 上设置 overrideUserInterfaceStyle 的时候,就会影响 window 下所有的 controller, view,包括后续推出的 controller。

我们回到刚刚的问题上,如果 App 打开夜间模式,那么很简单我们只需要设置 windowoverrideUserInterfaceStyle 属性就好了。

题外话: 当我们用 Xcode11 创建项目,我们会发现项目结构发生了变化,windowAppDelegate 移到 SceneDelegate 中。那么如何获取 SceneDelegate 中的 window 呢,代码如下

// 这里就简单介绍一下,实际项目中,如果是iOS应用这么写没问题,但是对于iPadOS应用还需要判断scene的状态是否激活
let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .dark

其他内容

Status Bar

之前 Status Bar 有两种状态,defaultlightContent

现在 Status Bar 有三种状态,default, darkContentlightContent

现在的 darkContent 对应之前的 default,现在的 default 会根据情况自动选择 darkContentlightContent

UIActivityIndicatorView

之前的 UIActivityIndicatorView 有三种 style 分别为 whiteLarge, whitegray现在全部废弃

增加两种 style 分别为 mediumlarge,指示器颜色用 color 属性修改。

如何在模式切换时打印日志

Arguments 中的 Arguments Passed On Launch 里面添加下面这行命令。

-UITraitCollectionChangeLoggingEnabled YES


以上是 iOS 13 如何适配 Dark Mode 的全部内容,如有错误欢迎指出。

WWDC链接 Implementing Dark Mode on iOS

如果你想知道 iOS 13 还增加了什么新特性可以阅读这篇文章