最近公司業務需求要更換APP主題。最開始是一個地方一個地方去改,而且項目中很多老代碼是用xib寫的,習慣純代碼編程的我改的很難受。而且以后指不定要再次更改主題。
于是我定義了幾個主要顏色的宏,代碼中只要是設置顏色的地方就用宏。這樣只需要改一次,當要切換主題的時候直接對宏進行更改就行了。
結合已做好的切換主題功能,再加上一個暗黑模式判斷,如果當前是暗黑模式就用A套色值,如果不是就用B套色值,這樣就實現了暗黑模式的適配了。
一、定義的宏:
代碼中只要是設置顏色的地方就用定義好的顏色。(下面個別宏只是我的項目場景中會使用到的,并不適用于所有APP,可自行針對自己的項目定義。有些顏色兩種模式下沒有變動)
/// 暗黑模式 YES是
#define CKDarkMode @available(iOS 13.0, *) && UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark
// MARK: - 十六進制顏色
#define HexOf(rgbValue) Hex_A(rgbValue,1.0)
#define Hex_A(rgbValue,a) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:a]
// MARK: - 用全局變量設置背景、文字,可以優雅的主題切換 (取全局唯一性的名稱,便于維護;最前面的優先級最高)
#define Color_Bg? ? ? ? CKDarkMode?HexOf(0x191C32):HexOf(0xf4f4f4) //背景主題顏色? ? 黑色/白色
#define Color_ContView? CKDarkMode?HexOf(0x1D213B):HexOf(0xffffff) //內容、cell顏色? 深藍色/白色 如果背景和cell顏色一樣,就都用這個
#define Color_Title? ? CKDarkMode?HexOf(0xFFFFFF):HexOf(0x393939) //主文字顏色? ? ? 白色/黑色
#define Color_Subtitle? CKDarkMode?HexOf(0x999999):HexOf(0x999999) //副文字顏色? ? ? 淺白色/灰色
#define Color_Green? ? CKDarkMode?HexOf(0x45C98F):HexOf(0x45C98F) //綠漲? ? ? ? ? (行情、交易)
#define Color_Red? ? ? CKDarkMode?HexOf(0xEF0C47):HexOf(0xEF0C47) //紅跌? ? ? ? ? (行情、交易)
//
#define Color_NavBg? ? CKDarkMode?HexOf(0x1D213B):HexOf(0xffffff) //導航欄背景顏色
#define Color_NavTitle? CKDarkMode?HexOf(0xFFFFFF):HexOf(0x393939) //導航欄標題顏色
#define Color_TabbarBg? CKDarkMode?HexOf(0x1D213B):HexOf(0xffffff) //標簽欄背景顏色
#define Color_Selected? CKDarkMode?HexOf(0x46CA8F):HexOf(0x46CA8F) //綠色 (按鈕選中、已認證狀態的顏色)
#define Color_Line? ? ? CKDarkMode?HexOf(0x191C32):HexOf(0xf4f4f4) //分割線
#define Color_DarkGray? CKDarkMode?HexOf(0x333333):HexOf(0x333333) //深灰色
#define Color_Gray? ? ? CKDarkMode?HexOf(0x666666):HexOf(0x666666) //灰色
#define Color_LightGray CKDarkMode?HexOf(0x999999):HexOf(0x999999) //淺灰色
#define Color_InputBg? CKDarkMode?HexOf(0x191C32):HexOf(0xf4f4f4) //輸入框背景顏色
#define Color_DarkBlue? CKDarkMode?HexOf(0x191C32):HexOf(0x191C32) //深藍色 (特殊顏色)
#define Color_HalfTitle CKDarkMode?Hex_A(0x999999, 0.5):Hex_A(0x999999, 0.5);//半透明文字 色值是副標題的一半
如果想關閉暗黑模式,直接設置:
#define CKDarkMode NO
二、遇到的問題:
1、最開始我是用的self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark去做判斷,但是有些類并沒有UITraitCollection這個屬性,很多地方報錯。
解決方案:
改用UITraitCollection.current屬性來獲取當前App的顏色模式。
2、CGColorRef相關:
bt.layer.borderColor = Color_Selected.CGColor;
報錯:
Incompatible operand types ('UIColor * _Nonnull' and 'CGColorRef _Nonnull' (aka 'struct CGColor *'))
解決方案:
UIColor *color = Color_Selected;
bt.layer.borderColor = color.CGColor;
3.每次打開APP都能展示正常的模式;但是如果打開APP后再切換模式,已經加載出來的頁面依然會顯示切換之前的主題模式。
解決方案:
在頁面中添加通知,獲取到切換主題的通知后重新刷新一下頁面顏色(類似于項目中的國際化通知處理邏輯)
4.項目中個別頁面的狀態欄是固定的白色,在切換頁面的時候會把狀態欄切換回主題顏色黑色,但是在暗黑模式下就會有問題,因為暗黑模式下整個APP的狀態欄都是白色的,這時不需要切換回黑色。
解決方案:
添加一個UIStatusBarStyle變量記錄主題狀態欄顏色,這樣可以不用在控制器內做太多額外的判斷。如果用 @available(iOS 13.0, *) 去做判斷,需求變更后還要每個地方都去改動代碼。用了這種方式,后面即使更改了主題或者關閉了暗黑模式,也不用一一去改代碼;也可以通過上面定義的宏CKDarkMode做判斷,關閉暗黑模式時只需把CKDarkMode設置為NO就行
UIStatusBarStyle _themeStatusBarStyle;//記錄主題狀態欄顏色
- (void)viewWillAppear:(BOOL)animated {
? ? [super viewWillAppear:animated];
? ? _themeStatusBarStyle = [UIApplication sharedApplication].statusBarStyle;
? ? // 設置狀態欄顏色為白色
? ? [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent;
}
- (void)viewWillDisappear:(BOOL)animated{
? ? [super viewWillDisappear:animated];
? ? // 恢復狀態欄顏色為主題顏色
? ? [UIApplication sharedApplication].statusBarStyle = _themeStatusBarStyle;
}
5、使用了宏的地方都會報警告,提示我要做系統版本判斷,但是實際上我已經在CKDarkMode中判斷過了,系統檢測不到:
'currentTraitCollection' is only available on iOS 13.0 or newer
解決方案:使用UIColor擴展。
999+的警告有點影響代碼視覺體驗,后面應該會改用擴展的方式。如果有更好的解決方案請在下方留言。
三、UITraitCollection介紹:
1、在 iOS 13 中,我們可以通過 UITraitCollection 來判斷當前系統的模式。UIView 和 UIViewController 、UIScreen、UIWindow都已經遵從了UITraitEnvironment這個協議,因此這些類都擁有一個叫做?traitCollection的屬性,在這些類中,我們可以這樣去判斷當前 App 的顏色模式:
BOOL isDark = (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark);
2、另外,我們還可以通過?UITraitCollection.current這個屬性來獲取當前 App 的顏色模式。
3、如果暫時不想開放這個功能,可以先暫時全局關閉暗黑模式:
在 Info.plist 文件中,添加 key 為 User Interface Style,類型為 String,value 設置為?Light (Dark)即可,如果重新打開就把這條設置刪除。(這種方式是在APP整個生命周期內關閉了暗黑模式;上面的設置#define CKDarkMode NO只是用代碼做了判斷并只在用了宏的地方起作用)。
4、在 iOS 13中,UIView、UIViewController 、UIWindow有了一個?overrideUserInterfaceStyle的新屬性,可以覆蓋系統的模式。
單個頁面或視圖關閉暗黑模式,設置?overrideUserInterfaceStyle為對應的模式,強制限制該視圖與其子視圖以設置的模式進行展示,不跟隨系統模式改變進行改變。
self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
設置此屬性會影響當前view / viewController / window以及它下面的任何內容。
如果你希望一個子視圖監聽系統的模式,請將?overrideUserInterfaceStyle屬性設置為.unspecified。
四、拓展
除了我的這種實現方案,還有其他方案可以適配暗黑模式:
1、UIColor擴展:
+(UIColor *)generateDynamicColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor{
????if(@available(iOS 13.0, *)) {
? ? ? ? UIColor *dyColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
????????????if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) {
????????????????return lightColor;
? ? ? ? ? ? }else{
????????????????return darkColor;
? ? ? ?}}];
????????return dyColor;
? ? }else{
????????return lightColor;
}}
問題:
這種寫法要在每個使用的地方分別傳一個普通模式的顏色和暗黑模式的顏色,不方便維護。
解決方案:
可以定義幾個常用顏色函數,特殊的場景就用上面的方法,這樣就不需要在每個地方都控制顏色值了。
+(UIColor *)ContViewColor{
????if(@available(iOS 13.0, *)) {
? ? ? ? UIColor *dyColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
????????????if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
????????????????return ColorA;? ? ? ? ? ?
????????????}else{
????????????????return ColorB; ? ? ? ? ??
???????}?}];
????????return dyColor;? ?
????}else{
????????return ColorB; ??
}}
2、可以在?Images.xcassets中定義幾種常用顏色,并為顏色再配置一個用于暗黑模式的對應的顏色:
3、在?Images.xcassets中配置不同模式下的圖片,當你設置為暗黑模式后就會自動顯示對應的圖片
4、啟動圖:
LaunchScreen.storyboard可以像普通的圖片那樣針對深色模式設置另外的一張圖片
5、layer:
UIColor *resolvedColor = [[UIColor labelColor] resolvedColorWithTraitCollection:self.view.traitCollection];
label.layer.borderColor = resolvedColor.CGColor;
6、UIActivityIndicatorView 的 style:
iOS 13前 的 UIActivityIndicatorViewStyle:
typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) {?
? ? UIActivityIndicatorViewStyleWhiteLarge,
? ? UIActivityIndicatorViewStyleWhite,
? ? UIActivityIndicatorViewStyleGray,
};
iOS 13后,由于暗黑模式,上述三個屬性都被廢棄,建議使用如下 style:
typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) {
????UIActivityIndicatorViewStyleMedium,
????UIActivityIndicatorViewStyleLarge,
????UIActivityIndicatorViewStyleWhiteLarge API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleLarge",
????UIActivityIndicatorViewStyleWhite API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleMedium",
????UIActivityIndicatorViewStyleGray API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleMedium",
};
7、Status Bar 的 style :
在 iOS 13 之前,狀態欄的樣式的枚舉值也帶有著明顯的顏色傾向,UIStatusBarStyleDefault、UIStatusBarStyleLightContent。
現在,狀態欄的 default 樣式會根據當前的模式展示不同的顏色,而原有的?lightContent樣式則新增一個?darkContent的樣式與之對應。
typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
? ? UIStatusBarStyleDefault? ? ? = 0, // Automatically chooses light or dark content based on the user interface style
? ? UIStatusBarStyleLightContent = 1, // Light content,foruse on dark backgrounds? ?
????UIStatusBarStyleDarkContent? = 3, // Dark content,foruse on light backgrounds
};
8、SF Symbols
注意:
命名時要保證這個名字的全局唯一性,避免和項目中其他命名雷同,這樣可以保證全局搜索時搜索到的結果只有你想搜索的內容,便于維護。例如你取名RedColor,會搜索到很多其他沒用的信息。這種命名思路也可以用在其他地方。
除了改背景顏色、文字顏色,還需要替換圖標、圖片,這個需要UI配合切圖。
參考: