京東 App適配 iOS 暗黑模式業務實踐
以下文章來源於京東零售技術,作者平臺研發姚琦
什麼是暗黑模式?
iOS 13 蘋果推出了暗黑模式,暗黑模式在夜間可以更好的保護視力,也可以節省 App 電量消耗。但是 Apple 提供的暗黑模式只支援 iOS 13,為了給使用者帶來更好的體驗,我們希望 iOS 13 以下的系統也可以支援暗黑模式。另外我們還給使用者提供了自主選擇的權利,可以在 App 內手動關閉暗黑模式,不跟隨系統主題變化。京東 App 涉及業務模組眾多,整個適配工作量巨大,為了解決上述問題,並讓各模組透過統一的介面快速接入,我們開發了暗黑基礎元件,提供以下能力:
- 支援 iOS 9 及以上系統,同時相容 iOS 13 系統暗黑模式
- 支援整體切量、降級
- 支援跟隨系統模式,也可以選擇不跟隨,使用 App 內部的模式
- 內建除錯工具,幫助開發者快速除錯,提升效率
- 支援顏色模式擴充套件
基礎元件設計方案如下:
業務接入
業務接入時需要呼叫基礎元件提供的jdbappearance_bindUpdater方法,傳入一個Block並在其中處理UI更新的邏輯,基礎元件會繫結Block和UIView,然後將UIView儲存在HashTable中,在合適的時機透過遍歷HashTable和執行繫結的Block來更新UI。業務元件的接入方案如下:
需要注意的是,遍歷HashTable的時候並不是所有的Block都會執行,這裡會判斷UIView的window是否存在,如果window有值,就執行UIView繫結的Block,否則會先把這個Block標記為稍後執行,當UIView下次出現在window中時(didMoveToWindow 被呼叫的時候)就會執行這個Block。另外不用擔心Block會在每次 didMoveToWindow 時被呼叫,因為只有顏色模式變化的時候,Block才會被標記為稍後執行。
如果涉及介面呼叫等非同步場景,是否會增加接入成本呢?我們透過下面的程式碼示例看一下業務是如何進行適配的:
1// 接入前
2cell.viewA.backgroundColor = [UIColor redColor];
3cell.viewB.image = [UIImage imageNamed:@"xxx"];
4
5
6// 接入後
7@weakify(cell)
8[cell jdbappearance_bindUpdater:^(JDBAppearance *apperance, UIView *bindView) {
9 @strongify(cell)
10 cell.viewA.backgroundColor = [UIColor jdbappearance_colorBR];
11 cell.viewB.image = [UIImage jdbappearance_imageNamed:@[@"light_xx", @"dark_xx"]];
12}];
因為每次呼叫jdbappearance_bindUpdater 時,會立刻執行一次Block,所以不論是否涉及非同步場景,接入方式都是統一的,並不會帶來額外的接入成本。
自定義Updater:
Block機制基本可以滿足所有的適配場景,但是實際開發中,我們可能希望有一些便捷的方法,比如直接呼叫一個方法jd_setBackgroundColor設定UIView的背景色。
這樣的需求也是可以滿足的,我們來看一下如何封裝這樣的API:
1@implementation UIView (CustomUpdater)
2
3
4- (void)jdb_setBackgroundColor:(NSArray *)colors
5{
6 [self jdbappearance_bindUpdater:^(JDBAppearance * _Nonnull appearance, UIView * _Nonnull bindView) {
7 bindView.backgroundColor = [UIColor jdbappearance_colorWithHex:colors];
8 } updaterKey:@"jdb_setBackgroundColor"];
9}
10
11
12@end
注意繫結Block的時候需要指定一個updaterKey,updaterKey允許一個UIView繫結多個Block。使用方式也很簡單,並且不需要考慮迴圈引用的問題:
1[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的傳遞路線:
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 的邏輯:
1@implementatiton ObserveWindow
2
3
4- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
5{
6 if (@available(iOS 13.0, *)) {
7 if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
8 // 1. 修改 App 內部樣式
9 // 2. 修改其他 window 的 overrideUserInterfaceStyle
10 // 3. 通知業務更新 UI
11 }
12 }
13}
14
15
16@end
多工介面快照
在適配過程中,我們發現一個問題:在多工介面,會出現 App 展示的顏色和系統顏色模式剛好相反。
進一步分析後,發現 App 在進入後臺時,traitCollectionDidChange 執行了2次,這兩次執行過程中系統的 userInterfaceStyle 分別是 UIUserInterfaceStyleDark 和 UIUserInterfaceStyleLight。
這是為什麼呢?我們檢視了下traitCollectionDidChange被呼叫時的堆疊:
看了堆疊就明白了,系統在進入後臺時會建立快照,這個快照其實就是系統多工介面展示的快照,呼叫2次是為了分別對深色和淺色進行快照,當進入多工介面時,系統會根據當前的顏色模式展示正確的快照。
為什麼我們會遇到顏色模式相反的問題呢,這裡要先介紹一下“跟隨系統”的功能:
App 中有一個開關,用來控制是否跟隨系統顏色模式。當使用者首次選擇切換到暗黑模式,會預設開啟跟隨系統,此時 App 模式會和系統模式保持一致。如果關閉“跟隨系統”的開關,則不再監聽系統模式的切換,以 App 內使用者選擇的模式為準。
當關閉“跟隨系統”的開關後,App 內的顏色模式有可能和系統的不一致,當出現不一致的時候,快照就會出錯,比如Dark模式擷取了Light模式的圖。為了避免這種錯誤,我們加了一個判斷條件,只有“跟隨系統”開啟的情況下才會開啟快照功能。
修改後的traitCollectionDidChange實現如下:
1-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
2{
3 if (@available(iOS 13.0, *)) {
4 UIApplicationState state = [UIApplication sharedApplication].applicationState;
5 if (state == UIApplicationStateBackground) {
6 // 系統切換到後臺時,會對顏色模式取反截2張圖
7 JDBAppearanceManager *manager = [JDBAppearanceManager sharedInstance];
8 if (manager.followSystemMode) {
9 // 如果跟隨系統,就更新UI,系統會在UI更新完成後進行快照
10 }
11 } else {
12 // 觸發場景:系統控制中心切換模式、後臺進入前臺、Xcode除錯選單切換模式
13if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
14 // 1. 修改 App 內部樣式
15 // 2. 修改其他 window 的 overrideUserInterfaceStyle
16 // 3. 通知業務更新 UI
17 }
18 }
19 }
20}
個性化定製
基礎元件的定位,除了為京東 App 的暗黑模式適配提供支援,我們還希望可以給更多的 App 使用。暗黑基礎元件在支援現有功能的基礎上,也支援個性化定製功能或者API,接入方可以根據自己的需求靈活選擇:
- App 內部切換開關
- 多工快照
- 自定義 Updater
- 自定義顏色模式
希望大家不要重複採坑
本文詳細介紹了京東 App iOS 暗黑模式適配過程中踩過的坑,以及整個方案的實現原理,希望對大家有所幫助。
歡迎點選“ 京東智聯雲 ”瞭解更多精彩內容!
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912185/viewspace-2681027/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 京東小程式摺疊屏適配探索
- 實踐 | 為 Trackr app 適配大螢幕裝置APP
- 京東APP訂單業務Swift優化總結APPSwift優化
- iOS16新特性 | 靈動島適配開發與到家業務場景結合的探索實踐iOS
- 京東物流實時風控實踐
- 京東APP百億級商品與車關係資料檢索實踐 | 京東雲技術團隊APP
- 移動端適配-實踐篇
- 墨天輪訪談 | 京東雲曲藝偉:京東零售核心業務背後的資料庫實踐資料庫
- 京東雲Kubernetes叢集最佳實踐
- Flutter深色模式適配Flutter模式
- 適配iOS11--contentInsetAdjustmentBehavioriOS
- 設計模式-適配者模式設計模式
- Android QMUI實戰:實現APP換膚功能,並自動適配手機深色模式AndroidUIAPP模式
- 乾貨 | 京東雲部署Wordpress最佳實踐
- 京東到家的持續整合實踐之路
- 京東物流常態化壓測實踐
- 京東購物小程式cookie方案實踐Cookie
- 京東LBS推薦演算法實踐演算法
- 京東短網址高可用提升最佳實踐 | 京東雲技術團隊
- iOS MJRefresh適配ios11以及iPhoneXiOSiPhone
- 中介者設計模式——業務實踐設計模式
- 京東實時資料產品應用實踐
- iOS13簡單適配iOS
- iOS MVP模式重構實踐iOSMVP模式
- Flutter適配深色模式(DarkMode)Flutter模式
- 京東技術中臺的Flutter實踐之路Flutter
- 京東技術中臺Flutter實踐之路(二)Flutter
- 京東短網址高可用提升最佳實踐
- 618京東到家APP-門詳頁反爬實戰 | 京東雲技術團隊APP
- 京東雲開發者|提高IT運維效率,深度解讀京東雲AIOps落地實踐運維AI
- 京東數科:2020年京東區塊鏈技術實踐白皮書(附下載)區塊鏈
- 「實操」適配 NebulaGraph 新版本與壓測實踐
- 同時適配安卓和IOS移動APP,能深度整合釘釘和企業微信安卓iOSAPP
- 快速適配 Flutter 之深色模式Flutter模式
- Android深色模式適配原理分析Android模式
- 談談Flutter適配深色模式Flutter模式
- 達達埋點遷移京東子午線實踐
- 乾貨 | 京東雲賬號安全管理最佳實踐