京東 App適配 iOS 暗黑模式業務實踐

Alt

如下文章來源於京東零售技術,做者平臺研發姚琦app

什麼是暗黑模式?

iOS 13 蘋果推出了暗黑模式,暗黑模式在夜間能夠更好的保護視力,也能夠節省 App 電量消耗。可是 Apple 提供的暗黑模式只支持 iOS 13,爲了給用戶帶來更好的體驗,咱們但願 iOS 13 如下的系統也能夠支持暗黑模式。另外咱們還給用戶提供了自主選擇的權利,能夠在 App 內手動關閉暗黑模式,不跟隨系統主題變化。異步

京東 App 涉及業務模塊衆多,整個適配工做量巨大,爲了解決上述問題,並讓各模塊經過統一的接口快速接入,咱們開發了暗黑基礎組件,提供如下能力:ide

  • 支持 iOS 9 及以上系統,同時兼容 iOS 13 系統暗黑模式
  • 支持總體切量、降級
  • 支持跟隨系統模式,也能夠選擇不跟隨,使用 App 內部的模式
  • 內置調試工具,幫助開發者快速調試,提高效率
  • 支持顏色模式擴展

基礎組件設計方案以下:
Alt工具

業務接入spa

業務接入時須要調用基礎組件提供的jdbappearance_bindUpdater方法,傳入一個Block並在其中處理UI更新的邏輯,基礎組件會綁定Block和UIView,而後將UIView存儲在HashTable中,在合適的時機經過遍歷HashTable和執行綁定的Block來更新UI。業務組件的接入方案以下:設計

Alt

須要注意的是,遍歷HashTable的時候並非全部的Block都會執行,這裏會判斷UIView的window是否存在,若是window有值,就執行UIView綁定的Block,不然會先把這個Block標記爲稍後執行,當UIView下次出如今window中時(didMoveToWindow 被調用的時候)就會執行這個Block。3d

另外不用擔憂Block會在每次 didMoveToWindow 時被調用,由於只有顏色模式變化的時候,Block纔會被標記爲稍後執行。調試

若是涉及接口調用等異步場景,是否會增長接入成本呢?咱們經過下面的代碼示例看一下業務是如何進行適配的:code

// 接入前
 cell.viewA.backgroundColor = [UIColor redColor];
 cell.viewB.image = [UIImage imageNamed:@"xxx"];
 
 
 // 接入後
 @weakify(cell)
 [cell jdbappearance_bindUpdater:^(JDBAppearance *apperance, UIView *bindView) {
     @strongify(cell)
    cell.viewA.backgroundColor = [UIColor jdbappearance_colorBR];
    cell.viewB.image = [UIImage jdbappearance_imageNamed:@[@"light_xx", @"dark_xx"]];
}];

由於每次調用jdbappearance_bindUpdater 時,會馬上執行一次Block,因此不管是否涉及異步場景,接入方式都是統一的,並不會帶來額外的接入成本。對象

自定義Updater:

Block機制基本能夠知足全部的適配場景,可是實際開發中,咱們可能但願有一些便捷的方法,好比直接調用一個方法jd_setBackgroundColor設置UIView的背景色。

這樣的需求也是能夠知足的,咱們來看一下如何封裝這樣的API:

@implementation UIView (CustomUpdater)
 
 
 - (void)jdb_setBackgroundColor:(NSArray *)colors
 {
     [self jdbappearance_bindUpdater:^(JDBAppearance * _Nonnull appearance, UIView * _Nonnull bindView) {
         bindView.backgroundColor = [UIColor jdbappearance_colorWithHex:colors];
     } updaterKey:@"jdb_setBackgroundColor"];
 }


@end

注意綁定Block的時候須要指定一個updaterKey,updaterKey容許一個UIView綁定多個Block。使用方式也很簡單,而且不須要考慮循環引用的問題:

[cell jdb_setBackgroundColor:@[@"#FFFFFF", @"#1D1B1B"]];

App內切換暗黑模式

這個功能容許用戶在 App 內手動開啓或者關閉暗黑模式,可是存在一個問題:

若是系統開啓了暗黑,可是 App 內關閉了,此時一些系統控件的顏色仍然是深色的(例如經過UIImagePickerController調起的系統相冊),從而致使系統控件顏色和 App 顏色不一致。

在闡述解決方案以前,先來介紹一下UITraitCollection:

UITraitCollection是 iOS 8 開始新增的一個類,管理着 App 中的用戶界面相關的一些系統特徵,每一個視圖都擁有本身的UITraitCollection。

iOS 13 顏色模式相關的信息,就存儲在userInterfaceStyle屬性中。若是咱們想給視圖單獨指定userInterfaceStyle,須要使用 iOS 13 新增的 API overrideUserInterfaceStyle,另外設置overrideUserInterfaceStyle是對子視圖生效的。

但是這麼多視圖,咱們應該修改誰的屬性呢?下面這張圖描述了視圖之間的層級關係以及UITraitCollection的傳遞路線:

Alt

UITraitCollection是自上而下傳遞的,可是 UIScreen 和 UIWindowScene 並未提供 overrideUserInterfaceStyle 這個API,咱們只能修改UIWindow的屬性,使UIWindow及其全部子視圖展現咱們設置的顏色:

  • 若是開啓了暗黑,將全部window的overrideUserInterfaceStyle設置爲
    UIUserInterfaceStyleDark。
  • 若是關閉了暗黑,將全部window的overrideUserInterfaceStyle設置爲
    UIUserInterfaceStyleLight。

若是在 overrideUserInterfaceStyle 修改後,又有新的 window 出現,這種狀況要怎麼處理呢?咱們註冊了UIWindowDidBecomeVisibleNotification通知,這個通知會在一個 UIWindow 對象變爲可見的時候發出,在接收到通知後,設置這個window的overrideUserInterfaceStyle屬性。

總結:經過修改window的overrideUserInterfaceStyle屬性,大多數系統控件的顏色都能和App的顏色保持一致。

監聽系統模式切換

爲何要提這個呢?用traitCollectionDidChange監聽不就能夠了嗎?

由於咱們發現,在修改overrideUserInterfaceStyle後,當切換系統顏色模式時,window及其子視圖的traitCollectionDidChange並無被調用。

雖然官方文檔中並無找到明確的說明,可是通過驗證,只要咱們將window的 overrideUserInterfaceStyle設置爲UIUserInterfaceStyleDark 或 UIUserInterfaceStyleLight,window 及其子視圖咱們都無法監聽。只有默認的UIUserInterfaceStyleUnspecified纔會生效。

那怎麼辦呢?咱們剛剛把全部window的 overrideUserInterfaceStyle都改了😂😂😂

辦法總比困難多!仔細來分析一下,咱們修改window的overrideUserInterfaceStyle是爲了同步修改系統控件的顏色。那咱們是否是能夠建立一個獨立的ObserveWindow,在切換模式的時候,若是是ObserveWindow就跳過,只修改其餘window的overrideUserInterfaceStyle。這樣就能夠在ObserveWindow中實現traitCollectionDidChange方法,處理監聽系統模式切換以及更新 App UI 的邏輯:

@implementatiton ObserveWindow
 
 
 - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 {
     if (@available(iOS 13.0, *)) {
         if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
             // 1. 修改 App 內部樣式
             // 2. 修改其餘 window 的 overrideUserInterfaceStyle
            // 3. 通知業務更新 UI
        }
    }
}


@end

多任務界面快照

在適配過程當中,咱們發現一個問題:在多任務界面,會出現 App 展現的顏色和系統顏色模式恰好相反。

進一步分析後,發現 App 在進入後臺時,traitCollectionDidChange 執行了2次,這兩次執行過程當中系統的 userInterfaceStyle 分別是 UIUserInterfaceStyleDark 和 UIUserInterfaceStyleLight。

這是爲何呢?咱們查看了下traitCollectionDidChange被調用時的堆棧:

Alt

看了堆棧就明白了,系統在進入後臺時會建立快照,這個快照其實就是系統多任務界面展現的快照,調用2次是爲了分別對深色和淺色進行快照,當進入多任務界面時,系統會根據當前的顏色模式展現正確的快照。

Alt

爲何咱們會遇到顏色模式相反的問題呢,這裏要先介紹一下「跟隨系統」的功能:

App 中有一個開關,用來控制是否跟隨系統顏色模式。當用戶首次選擇切換到暗黑模式,會默認開啓跟隨系統,此時 App 模式會和系統模式保持一致。若是關閉「跟隨系統」的開關,則再也不監聽系統模式的切換,以 App 內用戶選擇的模式爲準。

當關閉「跟隨系統」的開關後,App 內的顏色模式有可能和系統的不一致,當出現不一致的時候,快照就會出錯,好比Dark模式截取了Light模式的圖。爲了不這種錯誤,咱們加了一個判斷條件,只有「跟隨系統」開啓的狀況下才會開啓快照功能。

修改後的traitCollectionDidChange實現以下:

-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 {
     if (@available(iOS 13.0, *)) {
   UIApplicationState state = [UIApplication sharedApplication].applicationState;
         if (state == UIApplicationStateBackground) {
             // 系統切換到後臺時,會對顏色模式取反截2張圖
             JDBAppearanceManager *manager = [JDBAppearanceManager sharedInstance];
             if (manager.followSystemMode) {
                 // 若是跟隨系統,就更新UI,系統會在UI更新完成後進行快照
            }
        } else {
            // 觸發場景:系統控制中心切換模式、後臺進入前臺、Xcode調試菜單切換模式
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
                // 1. 修改 App 內部樣式
                // 2. 修改其餘 window 的 overrideUserInterfaceStyle
                // 3. 通知業務更新 UI
            }
        }
    }
}

個性化定製

基礎組件的定位,除了爲京東 App 的暗黑模式適配提供支持,咱們還但願能夠給更多的 App 使用。暗黑基礎組件在支持現有功能的基礎上,也支持個性化定製功能或者API,接入方能夠根據本身的需求靈活選擇:

  • App 內部切換開關
  • 多任務快照
  • 自定義 Updater
  • 自定義顏色模式

### 但願你們不要重複採坑

本文詳細介紹了京東 App iOS 暗黑模式適配過程當中踩過的坑,以及整個方案的實現原理,但願對你們有所幫助。

歡迎點擊「京東智聯雲」瞭解更多精彩內容!

Alt

Alt

相關文章
相關標籤/搜索