今天的 WWDC 19 上發布了 iOS 13,我們來看下如何適配 DarkMode
首先我們來看下效果圖
如何適配 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.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 適配,是不是很簡單呢 (手動滑稽)
如何獲取當前模式 (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 都設置一遍嗎
答案是不需要,我們先來看一張圖。
當我們設置一個 controller 為 dark 之后,這個 controller 下的 view,都會是 dark mode,但是后續 present 的 controller 仍然是跟隨系統的樣式。
因為蘋果對 overrideUserInterfaceStyle
屬性的解釋是這樣的。
當我們在一個普通的 controlle, view 上重寫這個屬性,只會影響當前的視圖,不會影響前面的 controller 和后續 present 的 controller。
但是當我們在 window
上設置 overrideUserInterfaceStyle
的時候,就會影響 window
下所有的 controller, view,包括后續推出的 controller。
但是當我們在 感謝 hostname 指出錯誤window.rootViewController
上設置 overrideUserInterfaceStyle
的時候,就會影響 rootViewController
下所有的 controller, view,包括后續推出的 controller。
我們回到剛剛的問題上,如果 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
以上是 iOS 13 如何適配 Dark Mode 的全部內容,如有錯誤歡迎指出。
WWDC鏈接 Implementing Dark Mode on iOS
如果你想知道 iOS 13 還增加了什么新特性可以閱讀這篇文章。