iOS13-適配夜間模式/深色外觀(Dark Mode)

今天的 WWDC 19 上發布了 iOS 13,我們來看下如何適配 DarkMode

首先我們來看下效果圖


效果圖.gif

如何適配 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.backgroundColorlabel,代碼如下

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)

我們可以看到,不管是顏色還是圖片,適配都是系統完成的,我們不用關心現在是什么樣的樣式。
但是在某些場景下,我們可能會有根據當前樣式來做一些其他適配的需求,這時我們就需要知道現在什么樣式。
我們可以在 UIViewControllerUIView 中調用 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 模式。
我們可以用以下代碼將當前 UIViewControllerUIView 的模式。

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 打開夜間模式,那么很簡單我們只需要設置 windowoverrideUserInterfaceStyle 屬性就好了。

題外話:當我們用 Xcode11 創建項目,我們會發現項目結構發生了變化,windowAppDelegate 移到 SceneDelegate 中。
那么如何獲取 SceneDelegate 中的 window 呢,代碼如下

// 這里就簡單介紹一下,實際項目中,如果是iOS應用這么寫沒問題,但是對于iPadOS應用還需要判斷scene的狀態是否激活
let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .dark

其他內容

Status Bar

之前 Status Bar 有兩種狀態,defaultlightContent
現在 Status Bar 有三種狀態,default, darkContentlightContent
現在的 darkContent 對應之前的 default,現在的 default 會根據情況自動選擇 darkContentlightContent

UIActivityIndicatorView

之前的 UIActivityIndicatorView 有三種 style 分別為 whiteLarge, whitegray現在全部廢棄
增加兩種 style 分別為 mediumlarge,指示器顏色用 color 屬性修改。

如何在模式切換時打印日志

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


以上是 iOS 13 如何適配 Dark Mode 的全部內容,如有錯誤歡迎指出。
WWDC鏈接 Implementing Dark Mode on iOS

如果你想知道 iOS 13 還增加了什么新特性可以閱讀這篇文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容