夜间模式是iOS13的重要更新之一,随之而来的是我们能从系统设置中“显示与亮度”中选择“浅色”、“深色”两种模式,并且可以设置自动切换。(“控制中心”亮度调节中也可直接调节)
首先看下效果图
已知问题:在系统设置为深色模式时候,无法更改StateBar颜色
1、如果不想适配深色模式
(1)直接在项目的plist文件中设置 UIUserInterfaceStyle UIUserInterfaceStyleLight
(2)在每个UIViewController或者BaseViewController(如果自己有的话),中设置
if (@available(iOS 13.0, *)){
self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
2、适配深色模式
首先我们要看一下显示模式的枚举值
typedef NS_ENUM(NSInteger, UIUserInterfaceStyle) {
UIUserInterfaceStyleUnspecified,
UIUserInterfaceStyleLight,
UIUserInterfaceStyleDark,
} API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);
DarkMode 主要从两个方面来适配,一是颜色,二是图片,适配的代码不是很多,接下来让我们一起来看看具体是怎么操作的吧。
iOS 13 之前 UIColor
只能表示一种颜色,从 iOS 13 开始 UIColor
是一个动态的颜色,它可以在 LightMode 和 DarkMode 拥有不同的颜色。
iOS 13 下 UIColor
增加了很多动态颜色,可以在UIInterface.h中查看
#pragma mark Foreground colors
/* Foreground colors for static text and related elements.
*/
@property (class, nonatomic, readonly) UIColor *labelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *secondaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *tertiaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *quaternaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
/* Foreground color for standard system links.
*/
@property (class, nonatomic, readonly) UIColor *linkColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
/* Foreground color for placeholder text in controls or text fields or text views.
*/
@property (class, nonatomic, readonly) UIColor *placeholderTextColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
/* Foreground colors for separators (thin border or divider lines).
* `separatorColor` may be partially transparent, so it can go on top of any content.
* `opaqueSeparatorColor` is intended to look similar, but is guaranteed to be opaque, so it will
* completely cover anything behind it. Depending on the situation, you may need one or the other.
*/
@property (class, nonatomic, readonly) UIColor *separatorColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *opaqueSeparatorColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
#pragma mark Background colors
/* We provide two design systems (also known as "stacks") for structuring an iOS app's backgrounds.
*
* Each stack has three "levels" of background colors. The first color is intended to be the
* main background, farthest back. Secondary and tertiary colors are layered on top
* of the main background, when appropriate.
*
* Inside of a discrete piece of UI, choose a stack, then use colors from that stack.
* We do not recommend mixing and matching background colors between stacks.
* The foreground colors above are designed to work in both stacks.
*
* 1. systemBackground
* Use this stack for views with standard table views, and designs which have a white
* primary background in light mode.
*/
@property (class, nonatomic, readonly) UIColor *systemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
/* 2. systemGroupedBackground
* Use this stack for views with grouped content, such as grouped tables and
* platter-based designs. These are like grouped table views, but you may use these
* colors in places where a table view wouldn't make sense.
*/
@property (class, nonatomic, readonly) UIColor *systemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
#pragma mark Fill colors
/* Fill colors for UI elements.
* These are meant to be used over the background colors, since their alpha component is less than 1.
*
* systemFillColor is appropriate for filling thin and small shapes.
* Example: The track of a slider.
*/
@property (class, nonatomic, readonly) UIColor *systemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
/* secondarySystemFillColor is appropriate for filling medium-size shapes.
* Example: The background of a switch.
*/
@property (class, nonatomic, readonly) UIColor *secondarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
/* tertiarySystemFillColor is appropriate for filling large shapes.
* Examples: Input fields, search bars, buttons.
*/
@property (class, nonatomic, readonly) UIColor *tertiarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
/* quaternarySystemFillColor is appropriate for filling large areas containing complex content.
* Example: Expanded table cells.
*/
@property (class, nonatomic, readonly) UIColor *quaternarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
#pragma mark Other colors
/* lightTextColor is always light, and darkTextColor is always dark, regardless of the current UIUserInterfaceStyle.
* When possible, we recommend using `labelColor` and its variants, instead.
*/
@property(class, nonatomic, readonly) UIColor *lightTextColor API_UNAVAILABLE(tvos); // for a dark background
@property(class, nonatomic, readonly) UIColor *darkTextColor API_UNAVAILABLE(tvos); // for a light background
/* groupTableViewBackgroundColor is now the same as systemGroupedBackgroundColor.
*/
@property(class, nonatomic, readonly) UIColor *groupTableViewBackgroundColor API_DEPRECATED_WITH_REPLACEMENT("systemGroupedBackgroundColor", ios(2.0, 13.0), tvos(13.0, 13.0));
@property(class, nonatomic, readonly) UIColor *viewFlipsideBackgroundColor API_DEPRECATED("", ios(2.0, 7.0)) API_UNAVAILABLE(tvos);
@property(class, nonatomic, readonly) UIColor *scrollViewTexturedBackgroundColor API_DEPRECATED("", ios(3.2, 7.0)) API_UNAVAILABLE(tvos);
@property(class, nonatomic, readonly) UIColor *underPageBackgroundColor API_DEPRECATED("", ios(5.0, 7.0)) API_UNAVAILABLE(tvos);
上面我们说到系统提供了一些动态的颜色供我们使用,但是在正常开发中,系统提供的颜色肯定是不够用的,所以我们要自己创建动态颜色。
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 适配,看起来还是很简单的呢。
我们可以看到,不管是颜色还是图片,适配都是系统完成的,我们不用关心现在是什么样的样式。
但是在某些场景下,我们可能会有根据当前样式来做一些其他适配的需求,这时我们就需要知道现在什么样式。
我们可以在 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。
但是当我们在 window.rootViewController
上设置 overrideUserInterfaceStyle
的时候,就会影响 rootViewController
下所有的 controller, view,包括后续推出的 controller。 感谢 hostname 指出错误
我们回到刚刚的问题上,如果 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