今天的 WWDC 19 上發(fā)布了 iOS 13,我們來(lái)看下如何適配 DarkMode
首先我們來(lái)看下效果圖

如何適配 DarkMode
DarkMode 主要從兩個(gè)方面來(lái)適配,一是顏色,二是圖片,適配的代碼不是很多,接下來(lái)讓我們一起來(lái)看看具體是怎么操作的吧。
顏色適配
iOS 13 之前 UIColor 只能表示一種顏色,從 iOS 13 開(kāi)始 UIColor 是一個(gè)動(dòng)態(tài)的顏色,它可以在 LightMode 和 DarkMode 擁有不同的顏色。
iOS 13 下 UIColor 增加了很多動(dòng)態(tài)顏色,我們來(lái)看下用系統(tǒng)提供的顏色能實(shí)現(xiàn)怎么樣的效果。
// 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

怎么樣,看起來(lái)和 iOS 13 之前設(shè)置一個(gè)顏色的方法一樣吧,用這種動(dòng)態(tài)顏色,系統(tǒng)直接替我們完成了適配的工作,是不是很方便呢。
如何自己創(chuàng)建一個(gè)動(dòng)態(tài)的 UIColor
上面我們說(shuō)到系統(tǒng)提供了一些動(dòng)態(tài)的顏色供我們使用,但是在正常開(kāi)發(fā)中,系統(tǒng)提供的顏色肯定是不夠用的,所以我們要自己創(chuàng)建動(dòng)態(tài)顏色。
iOS 13 下 UIColor 增加了一個(gè)初始化方法,我們可以用這個(gè)初始化方法來(lái)創(chuàng)建動(dòng)態(tài)顏色。
@available(iOS 13.0, *)
public init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)
這個(gè)方法要求傳一個(gè)閉包進(jìn)去,當(dāng)系統(tǒng)從 LightMode 和 DarkMode 之間切換的時(shí)候就會(huì)觸發(fā)這個(gè)回調(diào)。
這個(gè)閉包返回一個(gè) UITraitCollection 類(lèi),我們要用這個(gè)類(lèi)的 userInterfaceStyle 屬性。
userInterfaceStyle 是一個(gè)枚舉,聲明如下
@available(iOS 12.0, *)
public enum UIUserInterfaceStyle : Int {
case unspecified
case light
case dark
}
這個(gè)枚舉會(huì)告訴我們當(dāng)前是 LightMode or DarkMode
現(xiàn)在我們創(chuàng)建兩個(gè) 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
現(xiàn)在,我們做完了動(dòng)圖中背景色和文本顏色的適配,接下來(lái)我們看看圖片如何適配
圖片適配
打開(kāi) Assets.xcassets
把圖片拖拽進(jìn)去,我們可以看到這樣的頁(yè)面

然后我們?cè)谟覀?cè)工具欄中點(diǎn)擊最后一欄,點(diǎn)擊 Appearances 選擇 Any, Dark,如圖所示

我們把 DarkMode 的圖片拖進(jìn)去,如圖所示

最后我們加上 ImageView 的代碼
imageView.image = UIImage(named: "icon")
現(xiàn)在我們就已經(jīng)完成顏色和圖片的 DarkMode 適配,是不是很簡(jiǎn)單呢 (手動(dòng)滑稽)
如何獲取當(dāng)前模式 (Light or Dark)
我們可以看到,不管是顏色還是圖片,適配都是系統(tǒng)完成的,我們不用關(guān)心現(xiàn)在是什么樣的樣式。
但是在某些場(chǎng)景下,我們可能會(huì)有根據(jù)當(dāng)前樣式來(lái)做一些其他適配的需求,這時(shí)我們就需要知道現(xiàn)在什么樣式。
我們可以在 UIViewController 或 UIView 中調(diào)用 traitCollection.userInterfaceStyle 來(lái)獲取當(dāng)前視圖的樣式,代碼如下
if trainCollection.userInterfaceStyle == .dark {
// Dark
} else {
// Light
}
那么我們什么時(shí)候需要用這樣的方法做適配呢,比如說(shuō)當(dāng)我們使用 CGColor 的時(shí)候,上面說(shuō)到 UIColor 在 iOS 13 下變成了一個(gè)動(dòng)態(tài)顏色,但是 CGColor 仍然只能表示單一的顏色,所以當(dāng)我們使用到 CGColor 的時(shí)候,我們就可以用上面的方法做適配。
顏色
對(duì)于 CGColor 我們還有還有另一種適配方法,代碼如下
let resolvedColor = labelColor.resolvedColor(with: traitCollection)
layer.borderColor = resolvedColor.cgColor
resolvedColor 方法會(huì)根據(jù)傳遞進(jìn)去的 traitCollection 返回對(duì)應(yīng)的顏色。
圖片
對(duì)于 UIImage 我們也有類(lèi)似的方法,代碼如下
let image = UIImage(named: "icon")
let resovledImage = image?.imageAsset?.image(with: traitCollection)
如何監(jiān)聽(tīng)模式變化
上面我們說(shuō)了如何獲取當(dāng)前模式,但是我們要搭配監(jiān)聽(tīng)方法一起使用,當(dāng) light dark 模式切換的時(shí)候,要把上面的代碼再執(zhí)行一遍。系統(tǒng)為我們提供了一個(gè)回調(diào)方法,當(dāng) light dark 切換時(shí)就會(huì)觸發(fā)這個(gè)方法。
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
// 適配代碼
}
}
題外話(huà)
如果你覺(jué)得這樣為 CGColor 做適配很麻煩,那么不妨試試 XYColor 這個(gè)框架。
如何改變當(dāng)前模式
我們可以看到在動(dòng)圖中是直接改系統(tǒng)的模式,從而讓 App 的模式修改,但是對(duì)于某些有夜間模式功能的 App 來(lái)說(shuō),如果用戶(hù)打開(kāi)了夜間模式,那么即使現(xiàn)在系統(tǒng)是 light 模式,也要強(qiáng)制用 dark 模式。
我們可以用以下代碼將當(dāng)前 UIViewController 或 UIView 的模式。
overrideUserInterfaceStyle = .dark
print(traitCollection.userInterfaceStyle) // dark
我們可以看到設(shè)置了 overrideUserInterfaceStyle 之后,traitCollection.userInterfaceStyle 就是我們?cè)O(shè)置后的模式了。
需要給每一個(gè) Controller 和 View 都設(shè)置一遍嗎
答案是不需要,我們先來(lái)看一張圖。

當(dāng)我們?cè)O(shè)置一個(gè) controller 為 dark 之后,這個(gè) controller 下的 view,都會(huì)是 dark mode,但是后續(xù) present 的 controller 仍然是跟隨系統(tǒng)的樣式。
因?yàn)樘O(píng)果對(duì) overrideUserInterfaceStyle 屬性的解釋是這樣的。
當(dāng)我們?cè)谝粋€(gè)普通的 controlle, view 上重寫(xiě)這個(gè)屬性,只會(huì)影響當(dāng)前的視圖,不會(huì)影響前面的 controller 和后續(xù) present 的 controller。
但是當(dāng)我們?cè)?window 上設(shè)置 overrideUserInterfaceStyle 的時(shí)候,就會(huì)影響 window 下所有的 controller, view,包括后續(xù)推出的 controller。
但是當(dāng)我們?cè)? 感謝 hostname 指出錯(cuò)誤window.rootViewController 上設(shè)置 overrideUserInterfaceStyle 的時(shí)候,就會(huì)影響 rootViewController 下所有的 controller, view,包括后續(xù)推出的 controller。
我們回到剛剛的問(wèn)題上,如果 App 打開(kāi)夜間模式,那么很簡(jiǎn)單我們只需要設(shè)置 window 的 overrideUserInterfaceStyle 屬性就好了。
題外話(huà):當(dāng)我們用 Xcode11 創(chuàng)建項(xiàng)目,我們會(huì)發(fā)現(xiàn)項(xiàng)目結(jié)構(gòu)發(fā)生了變化,window 從 AppDelegate 移到 SceneDelegate 中。
那么如何獲取 SceneDelegate 中的 window 呢,代碼如下
// 這里就簡(jiǎn)單介紹一下,實(shí)際項(xiàng)目中,如果是iOS應(yīng)用這么寫(xiě)沒(méi)問(wèn)題,但是對(duì)于iPadOS應(yīng)用還需要判斷scene的狀態(tài)是否激活
let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .dark
其他內(nèi)容
Status Bar
之前 Status Bar 有兩種狀態(tài),default 和 lightContent
現(xiàn)在 Status Bar 有三種狀態(tài),default, darkContent 和 lightContent
現(xiàn)在的 darkContent 對(duì)應(yīng)之前的 default,現(xiàn)在的 default 會(huì)根據(jù)情況自動(dòng)選擇 darkContent 和 lightContent
UIActivityIndicatorView
之前的 UIActivityIndicatorView 有三種 style 分別為 whiteLarge, white 和 gray,現(xiàn)在全部廢棄。
增加兩種 style 分別為 medium 和 large,指示器顏色用 color 屬性修改。
如何在模式切換時(shí)打印日志
在 Arguments 中的 Arguments Passed On Launch 里面添加下面這行命令。
-UITraitCollectionChangeLoggingEnabled YES

以上是 iOS 13 如何適配 Dark Mode 的全部?jī)?nèi)容,如有錯(cuò)誤歡迎指出。
WWDC鏈接 Implementing Dark Mode on iOS
如果你想知道 iOS 13 還增加了什么新特性可以閱讀這篇文章。