iOS在13的版本加入了對深色模式的支持,深色模式下App整體上呈現(xiàn)黑色UI界面,現(xiàn)在許多App都完成了深色模式的適配,但也有少量App未支持深色模式(這些App大多是內(nèi)嵌較多的H5頁面)例如支付寶等,本文主要介紹iOS深色模式的適配。
System Colors
Apple為了適配深色模式對UIKit中的UIColor進(jìn)行了重新定義,例如將.red, .blue和 .yellow定義為.systemRed,.systemBlue和.systemYellow,這些新定義的System Colors在深色和淺色模式下表現(xiàn)為不同的顏色,具體的色值可以見蘋果的官方文檔。

Semantic Colors
對于一些需要進(jìn)行文字顯示的控件Apple也做了深色模式的適配,Apple新加了Semantic Colors顏色方案,使用Semantic Colors時(shí)不需要糾結(jié)具體的值,只需要在合適的場景使用,例如當(dāng)控件是Label時(shí),在沒有自定義字體顏色時(shí),可以使用.label類型的的Semantic Colors,在淺色模式下顯示黑色字體,在深色模式下顯示白色字體;Semantic Colors包括.label,.separator,.link, .systemBackground和.systemFill。
對于系統(tǒng)適配的以上深色模式下的顏色可以使用SemanticUI這個(gè)App進(jìn)行查看,該App給出了二種模式下的系統(tǒng)顏色的對比:

Dynamic Colors
當(dāng)然在實(shí)際開發(fā)中很多情況下我們都是需要自定義顏色的,有幸的是Apple也給出了相應(yīng)的方案,那就是通過UIColor.init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)這個(gè)方法進(jìn)行創(chuàng)建自定義的semantic color。
使用代碼的方式:
import UIKit
infix operator |: AdditionPrecedence
public extension UIColor {
static func | (lightMode: UIColor, darkMode: UIColor) -> UIColor {
guard #available(iOS 13.0) else { return lightMode }
return UIColor { (traitCollection) -> UIColor in
return traitCollection.userInterfaceStyle == .light ? lightMode : darkMode
}
}
}
// 使用
let dynamicColor = .black | .white
使用Asset Catalog方式
自動iOS11開始,可以在Asset Catalogs保存自定義顏色,并支持Interface Builder和code二種方式使用,自定義的color目前也支持深色模式。打開Assets.xcassets ,店家左下角的+號按鈕新增一個(gè)Color Set,在Any Appearence(淺色模式)和Dark Appearence(深色模式)分別添加一種顏色即可。

當(dāng)然也支持代碼的使用:
Let view = Uiview()
View.backdroundcolor = Color(named: Color)
Border colors
Border colors在當(dāng)主題模式發(fā)生改變時(shí)并不會自動的進(jìn)行適配,所以需要手動的進(jìn)行處理,可以通過traitCollectionDidChange(_:)這個(gè)方法在進(jìn)行處理:
import UIKit
extension UIColor {
static let layer = UIColor(light: .init(red: 106 / 255, green: 32 / 255, blue: 119 / 255, alpha: 1),
dark: .init(red: 138 / 255, green: 76 / 255, blue: 146 / 255, alpha: 1))
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
traitCollection.hasDifferentColorAppearance(comparedTo: traitCollection) {
layer.backgroundColor = UIColor.layer.cgColor
}
}
引起
TraitCollection變化有多種情況的,不只是系統(tǒng)顯示模式的切換,例如當(dāng)屏幕方向發(fā)生旋轉(zhuǎn)時(shí)也會觸發(fā)上面的方法,所以最好是做一個(gè)判斷,當(dāng)TraitCollection的更改影響color的值時(shí)才給layer賦值,在Xcode中可以添加一個(gè)launch argument:UITraitCollertionChangeLoggingEnabled = YES來檢測TraitCollection的改變:

Dynamic Images
圖片資源同樣支持深色模式,需要使用Assets.xcassets,新建一個(gè)Assets.xcassets并在Attributes inspector點(diǎn)擊Appearances選擇Any, Dark,然后分別為Any Appearances和Dark Appearances配置響應(yīng)的圖片。

實(shí)際開發(fā)中圖片資源可能是后臺返回的,這時(shí)候需要使用
image assets:
import UIKit
public extension UIImageAsset {
convenience init(lightModeImage: UIImage?, darkModeImage: UIImage?) {
self.init()
register(lightModeImage: lightModeImage, darkModeImage: darkModeImage)
}
func register(lightModeImage: UIImage?, darkModeImage: UIImage?) {
register(lightModeImage, for: .light)
register(darkModeImage, for: .dark)
}
func register(_ image: UIImage?, for traitCollection: UITraitCollection) {
guard let image = image else {
return
}
register(image, with: traitCollection)
}
func image() -> UIImage {
if #available(iOS 13.0, tvOS 13.0, *) {
return image(with: .current)
}
return image(with: .light)
}
}
盡量減少圖片
為了適配深色模式,無限的增加圖片資源最終會導(dǎo)致包的大小會增加很多,為了減少包的體積,在非必要添加圖片的情況下有以下二種方案:
-
使用tint color
對于那些像toolbars和tab bars的圖片,可以用tint color去渲染這些圖片以滿足深色模式,首先需要讓圖片以模板的形式渲染:
或者:
let iconImage = UIImage()
let imageView = UIImageView()
imageView.image = iconImage.withRenderingMode(.alwaysTemplate)
使用:
public static var tint: UIColor = {
if #available(iOS 13, *) {
return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in
if UITraitCollection.userInterfaceStyle == .dark {
return Colors.osloGray
} else {
return Colors.dataRock
}
}
} else {
return Colors.dataRock
}
}()
imageView.tintColor = Style.Colors.tint
-
反轉(zhuǎn)圖片顏色
反轉(zhuǎn)圖片顏色也是一種好的選擇,只是不是對所有的圖片都合適,可以使用如下代碼:
extension UIImage {
func invertedColors() -> UIImage? {
guard let ciImage = CIImage(image: self) ?? ciImage, let filter = CIFilter(name: "CIColorInvert") else { return nil }
filter.setValue(ciImage, forKey: kCIInputImageKey)
guard let outputImage = filter.outputImage else { return nil }
return UIImage(ciImage: outputImage)
}
}
自動更新 Dark Mode
現(xiàn)在支持深色模式的App都會提供淺色和深色模式的選擇,同時(shí)支持跟誰系統(tǒng)自動切換,當(dāng)用戶選擇相應(yīng)模式后需要用Userdefaults保存用戶的選擇。

import UIKit
public extension UserDefaults {
var overridedUserInterfaceStyle: UIUserInterfaceStyle {
get {
UIUserInterfaceStyle(rawValue: integer(forKey: #function)) ?? .unspecified
}
set {
set(newValue.rawValue, forKey: #function)
}
}
}
當(dāng)保存了用戶選擇后同時(shí)需要更新當(dāng)前App的顯示:
public extension UIApplication {
func override(_ userInterfaceStyle: UIUserInterfaceStyle) {
// iPad支持多窗口,不支持iPad的話可以刪除這段判斷
if #available(iOS 13.0, *), supportsMultipleScenes {
for connectedScene in connectedScenes {
if let scene = connectedScene as? UIWindowScene {
scene.windows.override(userInterfaceStyle)
}
}
}
else {
windows.override(userInterfaceStyle)
}
}
}
public extension UIWindow {
func override(_ userInterfaceStyle: UIUserInterfaceStyle) {
if #available(iOS 13.0, tvOS 13.0, *) {
overrideUserInterfaceStyle = userInterfaceStyle
}
}
}
public extension Array where Element: UIWindow {
func override(_ userInterfaceStyle: UIUserInterfaceStyle) {
for window in self {
window.override(userInterfaceStyle)
}
}
}
UIApplication.shared.override(style)
UserDefaults.standard.overridedUserInterfaceStyle = style
退出或禁用深色模式
當(dāng)采用Xcode 11新建項(xiàng)目時(shí)會默認(rèn)開啟深色模式的,當(dāng)沒時(shí)間或者暫不支持深色模式時(shí)可以禁用掉,簡單的方法時(shí)在Info.plist中添加一個(gè)UIUserInterfaceStyle = Light,也支持為某個(gè)View或者ViewController單獨(dú)設(shè)置相應(yīng)的模式,但是這種設(shè)置不會影響模態(tài)彈出的VC的模式。
let view = UIView()
view.overrideUserInterfaceStyle = .dark
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .dark
}
}
總結(jié):
本文詳細(xì)介紹了
iOS深色模式的適配,最主要的是通過UITraitCollection來拿到當(dāng)前的樣式來進(jìn)行相應(yīng)的顏色上的適配,Apple也提供的新定義的幾種系統(tǒng)顏色方案并能自動適配深色模式,深色模式適配代碼不多,就是需要將項(xiàng)目所有的界面都要進(jìn)行適配,工作量巨大。
