背景
iOS 13蘋(píng)果公司推出了暗黑模式脸秽,APP默認(rèn)支持猩谊,用戶可以通過(guò)在設(shè)置-顯示與亮度-外觀欄中選擇深色來(lái)打開(kāi)暗黑模式揪罕,但是讹堤,如果開(kāi)發(fā)工程師不進(jìn)行適配吆鹤,應(yīng)用內(nèi)可能會(huì)出現(xiàn)某些視圖的顏色變成黑色,影響顯示效果洲守。
要防止這種情況可以給控制器或者視圖設(shè)置overrideUserInterfaceStyle
屬性為UIUserInterfaceStyleLight
或者UIUserInterfaceStyleDark
疑务,這樣當(dāng)前視圖和它的所有子視圖都會(huì)固定為Dark或者Light模式。也可以在info.plist中加入UIUserInterfaceStyle
鍵梗醇,給定Light
值知允,使整個(gè)應(yīng)用忽略暗黑模式。
蘋(píng)果公司在News And Updates這樣說(shuō):
If you need more time to make your apps look fantastic in Dark Mode, or if Dark Mode is not suited for your app, you can learn how to opt out.如果你需要更多的時(shí)間讓你的APP在暗黑模式下更加出色叙谨,或者暗黑模式不適合你的APP温鸽,你可以學(xué)習(xí)如何退出。
同時(shí)唉俗,適配暗黑模式是強(qiáng)烈建議的嗤朴,僅在適配暗黑模式的過(guò)程中配椭,使用UIUserInterfaceStyle
鍵暫時(shí)退出:
Choosing a Specific Interface Style for Your iOS App:Supporting Dark Mode is strongly encouraged. Use the UIUserInterfaceStyle
key to opt out only temporarily while you work on improvements to your app's Dark Mode support.
原理
蘋(píng)果公司使用UITraitCollection對(duì)象記錄界面環(huán)境特征,里面包含Size Class雹姊,Layout Direction股缸,User Interface Style信息(Dark或者Light)。每個(gè)UIView吱雏,UIViewController和UIPresentationController對(duì)象都持有這個(gè)對(duì)象敦姻。子視圖被添加到父視圖的時(shí)候,子視圖會(huì)繼承父視圖的UITraitCollection歧杏,UITraitCollection信息就從UIScreen一直傳遞到當(dāng)前顯示的UIView:UIScreen->UIWindow->UIPresentationViewController->UIViewController→UIView镰惦。
用戶更改了系統(tǒng)外觀后,系統(tǒng)通過(guò)調(diào)用以下方法重新渲染視圖犬绒,完成系統(tǒng)外觀的切換:
UIView:
traitCollectionDidChange(_:)
layoutSubviews()
draw(_:)
updateConstraints()
tintColorDidChange()
UIViewController:
traitCollectionDidChange(_:)
updateViewConstraints()
viewWillLayoutSubviews()
viewDidLayoutSubviews()
UIPresentationController:
traitCollectionDidChange(_:)
containerViewWillLayoutSubviews()
containerViewDidLayoutSubviews()
在這些方法調(diào)用前旺入,系統(tǒng)會(huì)更新UITraitCollection對(duì)象,所以要在這些方法中加入Dark模式和Light模式有區(qū)別的代碼凯力,如Dark模式下要在圖片上加一層遮罩茵瘾,Light則要隱藏。如果寫(xiě)在別的地方咐鹤,如在初始化方法或者viewDidLoad中拗秘,會(huì)造成模式切換后,遮罩還在祈惶,或者一直不顯示雕旨。
適配
在適配實(shí)踐中會(huì)總結(jié)出更好的實(shí)現(xiàn)方式,或者發(fā)現(xiàn)很多細(xì)節(jié)需要處理捧请,這些都會(huì)影響開(kāi)發(fā)時(shí)間凡涩。所以調(diào)研時(shí)編寫(xiě)Demo并根據(jù)實(shí)際項(xiàng)目調(diào)試效果是很有必要的。
顏色適配
顏色適配只要將UIColor對(duì)象改成動(dòng)態(tài)顏色對(duì)象即可血久。動(dòng)態(tài)顏色對(duì)象在不同的外觀下突照,有不同的顏色值。它也是UIColor對(duì)象氧吐,但是創(chuàng)建的方式不一樣。UIKit會(huì)根據(jù)UITraitCollection信息解析出對(duì)應(yīng)外觀的顏色值末盔。具體使用如下:
if (@available(iOS 13.0, *)) {
label.textColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [[UIColor secondarySystemBackgroundColor] resolvedColorWithTraitCollection:traitCollection];
} else {
return lightColor;
}
}];
} else {
label.textColor = lightColor;
};
colorWithDynamicProvider是創(chuàng)建動(dòng)態(tài)顏色的方法筑舅。resolvedColorWithTraitCollection是把動(dòng)態(tài)顏色解析成固定顏色的方法,在創(chuàng)建動(dòng)態(tài)顏色的block中不能返回動(dòng)態(tài)顏色陨舱,這里在Dark模式下使用了系統(tǒng)的secondarySystemBackgroundColor動(dòng)態(tài)顏色翠拣,所以返回時(shí)做了解析。
動(dòng)態(tài)顏色也可以通過(guò)Xcode創(chuàng)建游盲,步驟如下:
使用的時(shí)候用指定方法獲取误墓,如下:
<pre>if (@available(iOS 11.0, *)) {
label.textColor = [UIColor colorNamed:@"testColor"];
} else {
label.textColor = UIColor.redColor;
}</pre>
colorNamed
方法只支持iOS11以上版本蛮粮。
看起來(lái)使用很麻煩。具體項(xiàng)目中運(yùn)用可以封裝一下谜慌。封裝代碼案例如下:
#define MJCOLOR [MJDynamicColor shareInstance]
//所有動(dòng)態(tài)顏色獲取的地方,適配暗黑模式
@interface MJDynamicColor : NSObject
+ (instancetype)shareInstance;
//背景色
/// 一級(jí)背景色然想,如UIViewController的View的背景色,一般是四周都能接觸到屏幕的視圖的背景色
@property (nonatomic, strong) UIColor *mj_backgroundColor;
/// 二級(jí)背景色欣范,如UITableViewCell的背景色
@property (nonatomic, strong) UIColor *mj_secondaryBackgroundColor;
/// 三級(jí)背景色变泄,如UITableViewCell中button的背景色,一般是最上層的視圖的背景色
@property (nonatomic, strong) UIColor *mj_tertiaryBackgroundColor;
// UILabel的文字的顏色
/// 類(lèi)似一級(jí)標(biāo)題
@property (nonatomic, strong) UIColor *mj_labelColor;
/// 類(lèi)似二級(jí)標(biāo)題
@property (nonatomic, strong) UIColor *mj_secondaryLabelColor;
/// 類(lèi)似三級(jí)標(biāo)題
@property (nonatomic, strong) UIColor *mj_tertiaryLabelColor;
/// 類(lèi)似四級(jí)標(biāo)題
@property (nonatomic, strong) UIColor *mj_quaternaryLabelColor;
@end
@implementation MJDynamicColor
...此部分代碼省略恼琼,都是類(lèi)似下面代碼重寫(xiě)get方法妨蛹,使用懶加載
- (UIColor *)mj_labelColor {
if (!_mj_labelColor) {
UIColor *lightColor = [UIColor mjl_colorFromHexString:@"0x666666" alpha:1.0];
if (@available(iOS 13.0, *)) {
_mj_labelColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [[UIColor labelColor] resolvedColorWithTraitCollection:traitCollection];
} else {
return lightColor;
}
}];
} else {
_mj_labelColor = lightColor;
};
}
return _mj_labelColor;
}
@end
這樣有兩個(gè)好處:一是懶加載使得性能提高了;二是多個(gè)地方使用相同的顏色時(shí)更方便統(tǒng)一修改晴竞。
使用起來(lái)如下:
label.textColor = MJCOLOR.mj_labelColor;
以上使用方式都是創(chuàng)建動(dòng)態(tài)顏色蛙卤,也就是自定義的動(dòng)態(tài)顏色,蘋(píng)果的API也提供了官方的動(dòng)態(tài)顏色噩死,也稱(chēng)為語(yǔ)義顏色颤难,直接使用就可以,在UIInterface.h文件中可以看到甜滨。
圖片適配
圖片適配和顏色適配類(lèi)似乐严,也有動(dòng)態(tài)圖片的概念,通過(guò)XCode創(chuàng)建衣摩,在.xcassets文件中把圖片改成動(dòng)態(tài)圖片就行:
使用處的代碼不用修改昂验,還是通過(guò)imageNamed
方法獲取。這個(gè)是iOS13之前的方法艾扮,所以不用判斷系統(tǒng)版本
[self.leftCloseButton setImage:[UIImage imageNamed:@"feeds_back_white"]
在夜間模式下如果重新使用一張圖片既琴,會(huì)使得圖片資源大小翻倍,所以一般都是加一層遮罩泡嘴,特定情況下才使用新圖片甫恩。這種情況有種偷懶的方法:
#import "UIImageView+NightMask.h"
static const char *MJUIImageViewNightMaskKey = "MJUIImageViewNightMaskKey";
@implementation UIImageView (NightMask)
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (@available(iOS 13.0, *)) {
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
self.mj_nightMask.hidden = false;
} else {
self.mj_nightMask.hidden = true;
}
}
} else {
// Fallback on earlier versions
}
}
- (UIView *)mj_nightMask {
UIView *obj = objc_getAssociatedObject(self, MJUIImageViewNightMaskKey);
if (!obj) {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor mjl_colorFromHexString:@"0x000000" alpha:1.0];
view.alpha = 0.3;
[self addSubview:view];
view.frame = self.bounds;
objc_setAssociatedObject(self, MJUIImageViewNightMaskKey, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return obj;
}
- (void)didMoveToWindow:(UIWindow *)newWindow {
if (@available(iOS 13.0, *)) {
if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
self.mj_nightMask.hidden = false;
} else {
self.mj_nightMask.hidden = true;
}
} else {
// Fallback on earlier versions
}
}
@end
給UIImageView添加一個(gè)分類(lèi),重寫(xiě)traitCollectionDidChange
方法酌予,這個(gè)方法在traitCollection
更改時(shí)會(huì)調(diào)用磺箕,這時(shí)候加一個(gè)遮罩就可以了。重寫(xiě)了didMoveToWindow
方法的原因是抛虫,在Dark模式下啟動(dòng)APP松靡,不會(huì)顯示Dark模式(夜間模式)的外觀。因?yàn)?code>traitCollectionDidChange沒(méi)有調(diào)用建椰,這個(gè)方法在traitCollection
更改的時(shí)候才會(huì)調(diào)用雕欺。
總結(jié):
編寫(xiě)Demo時(shí)發(fā)現(xiàn)的很費(fèi)時(shí)間的點(diǎn),也發(fā)現(xiàn)給圖片或者顏色統(tǒng)一做處理行不通,夜間模式下的每個(gè)頁(yè)面都需要UI重新設(shè)計(jì)屠列,開(kāi)發(fā)也需要重新聯(lián)調(diào)每個(gè)頁(yè)面啦逆,需要的時(shí)間非常漫長(zhǎng)。
在顏色適配中笛洛,每個(gè)夜間(Dark)模式下給的顏色都要UI重新設(shè)計(jì)夏志,并和開(kāi)發(fā)聯(lián)調(diào),因?yàn)橐归g模式下撞蜂,不能隨便給一個(gè)對(duì)應(yīng)顏色盲镶,使用蘋(píng)果官方提供的動(dòng)態(tài)顏色效果也差(相當(dāng)于開(kāi)發(fā)人員自己來(lái)設(shè)計(jì)UI,并反復(fù)調(diào)試效果)蝌诡。所以需要給APP所有頁(yè)面重新設(shè)計(jì)一套夜間模式下的UI溉贿。
-
圖片適配中,單一處理行不通浦旱,也需要每個(gè)頁(yè)面單獨(dú)過(guò)UI如:
- 統(tǒng)一添加遮罩:本身就是起遮罩作用的UIImageView宇色。
- 統(tǒng)一添加遮罩:很多圖片的外邊緣部分是透明的,加遮罩后外邊緣不透明了颁湖,顯示出邊緣部分了宣蠕。
- 統(tǒng)一添加遮罩:有些圖片會(huì)動(dòng)態(tài)修改大小,但是遮罩不會(huì)跟隨著變動(dòng)甥捺。
- 給定新圖片 :新圖片也需要UI重新設(shè)計(jì)的時(shí)間抢蚀,因?yàn)橐WC和頁(yè)面其它部分協(xié)調(diào),直接給定一張單一方式處理的圖片镰禾,如只是改了下圖片的亮度皿曲,很可能跟頁(yè)面不協(xié)調(diào)。
折中方案:
- 只修改背景色吴侦,圖片和文字在用戶能正常使用的情況下都不做處理屋休,我看微信在夜間模式下的效果基本就是這樣。
- 整個(gè)APP不支持暗黑模式备韧,因?yàn)樘O(píng)果官方并沒(méi)有在APP Store Review Guidelines中規(guī)定某些類(lèi)型APP必須兼容暗黑模式劫樟,某些不需要兼容,而是沒(méi)有提到相關(guān)內(nèi)容织堂。