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

    夜间模式是iOS13的重要更新之一,随之而来的是我们能从系统设置中“显示与亮度”中选择“浅色”、“深色”两种模式,并且可以设置自动切换。(“控制中心”亮度调节中也可直接调节)

首先看下效果图

iOS13 适配夜间/深色模式(Dark Mode)_第1张图片

已知问题:在系统设置为深色模式时候,无法更改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

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);

如何自己创建一个动态的 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
把图片拖拽进去,我们可以看到这样的页面

iOS13 适配夜间/深色模式(Dark Mode)_第2张图片

然后我们在右侧工具栏中点击最后一栏,点击 Appearances 选择 Any, Dark,如图所示

iOS13 适配夜间/深色模式(Dark Mode)_第3张图片

我们把 DarkMode 的图片拖进去,如图所示

iOS13 适配夜间/深色模式(Dark Mode)_第4张图片

最后我们加上 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 都设置一遍吗

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

iOS13 适配夜间/深色模式(Dark Mode)_第5张图片


当我们设置一个 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 分别为 whiteLargewhite 和 gray现在全部废弃
增加两种 style 分别为 medium 和 large,指示器颜色用 color 属性修改。

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

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

iOS13 适配夜间/深色模式(Dark Mode)_第6张图片

 

你可能感兴趣的:(iOS_dev)