iOS:Dark Mode-暗黑模式調(diào)研

背景

iOS 13蘋果公司推出了暗黑模式,APP默認(rèn)支持,用戶可以通過在設(shè)置-顯示與亮度-外觀欄中選擇深色來打開暗黑模式,但是,如果開發(fā)工程師不進(jìn)行適配,應(yīng)用內(nèi)可能會出現(xiàn)某些視圖的顏色變成黑色,影響顯示效果。

要防止這種情況可以給控制器或者視圖設(shè)置overrideUserInterfaceStyle屬性為UIUserInterfaceStyleLight或者UIUserInterfaceStyleDark,這樣當(dāng)前視圖和它的所有子視圖都會固定為Dark或者Light模式。也可以在info.plist中加入UIUserInterfaceStyle鍵,給定Light值,使整個應(yīng)用忽略暗黑模式。

蘋果公司在News And Updates這樣說:

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.如果你需要更多的時間讓你的APP在暗黑模式下更加出色,或者暗黑模式不適合你的APP,你可以學(xué)習(xí)如何退出。

同時,適配暗黑模式是強烈建議的,僅在適配暗黑模式的過程中,使用UIUserInterfaceStyle鍵暫時退出:

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.

原理

蘋果公司使用UITraitCollection對象記錄界面環(huán)境特征,里面包含Size Class,Layout Direction,User Interface Style信息(Dark或者Light)。每個UIView,UIViewController和UIPresentationController對象都持有這個對象。子視圖被添加到父視圖的時候,子視圖會繼承父視圖的UITraitCollection,UITraitCollection信息就從UIScreen一直傳遞到當(dāng)前顯示的UIView:UIScreen->UIWindow->UIPresentationViewController->UIViewController→UIView。

用戶更改了系統(tǒng)外觀后,系統(tǒng)通過調(diào)用以下方法重新渲染視圖,完成系統(tǒng)外觀的切換:

UIView:
traitCollectionDidChange(_:)
layoutSubviews()
draw(_:)
updateConstraints()
tintColorDidChange()

UIViewController:
traitCollectionDidChange(_:)
updateViewConstraints()
viewWillLayoutSubviews()
viewDidLayoutSubviews()

UIPresentationController:
traitCollectionDidChange(_:)
containerViewWillLayoutSubviews()
containerViewDidLayoutSubviews()

在這些方法調(diào)用前,系統(tǒng)會更新UITraitCollection對象,所以要在這些方法中加入Dark模式和Light模式有區(qū)別的代碼,如Dark模式下要在圖片上加一層遮罩,Light則要隱藏。如果寫在別的地方,如在初始化方法或者viewDidLoad中,會造成模式切換后,遮罩還在,或者一直不顯示。

適配

在適配實踐中會總結(jié)出更好的實現(xiàn)方式,或者發(fā)現(xiàn)很多細(xì)節(jié)需要處理,這些都會影響開發(fā)時間。所以調(diào)研時編寫Demo并根據(jù)實際項目調(diào)試效果是很有必要的。

顏色適配

顏色適配只要將UIColor對象改成動態(tài)顏色對象即可。動態(tài)顏色對象在不同的外觀下,有不同的顏色值。它也是UIColor對象,但是創(chuàng)建的方式不一樣。UIKit會根據(jù)UITraitCollection信息解析出對應(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)建動態(tài)顏色的方法。resolvedColorWithTraitCollection是把動態(tài)顏色解析成固定顏色的方法,在創(chuàng)建動態(tài)顏色的block中不能返回動態(tài)顏色,這里在Dark模式下使用了系統(tǒng)的secondarySystemBackgroundColor動態(tài)顏色,所以返回時做了解析。

動態(tài)顏色也可以通過Xcode創(chuàng)建,步驟如下:

image.png

使用的時候用指定方法獲取,如下:

<pre>if (@available(iOS 11.0, *)) {
    label.textColor = [UIColor colorNamed:@"testColor"];
} else {
    label.textColor = UIColor.redColor;
}</pre>

colorNamed方法只支持iOS11以上版本。

看起來使用很麻煩。具體項目中運用可以封裝一下。封裝代碼案例如下:

#define MJCOLOR [MJDynamicColor shareInstance]
//所有動態(tài)顏色獲取的地方,適配暗黑模式
@interface MJDynamicColor : NSObject
+ (instancetype)shareInstance;
//背景色
/// 一級背景色,如UIViewController的View的背景色,一般是四周都能接觸到屏幕的視圖的背景色
@property (nonatomic, strong) UIColor *mj_backgroundColor;
/// 二級背景色,如UITableViewCell的背景色
@property (nonatomic, strong) UIColor *mj_secondaryBackgroundColor;
/// 三級背景色,如UITableViewCell中button的背景色,一般是最上層的視圖的背景色
@property (nonatomic, strong) UIColor *mj_tertiaryBackgroundColor;
// UILabel的文字的顏色
/// 類似一級標(biāo)題
@property (nonatomic, strong) UIColor *mj_labelColor;
/// 類似二級標(biāo)題
@property (nonatomic, strong) UIColor *mj_secondaryLabelColor;
/// 類似三級標(biāo)題
@property (nonatomic, strong) UIColor *mj_tertiaryLabelColor;
/// 類似四級標(biāo)題
@property (nonatomic, strong) UIColor *mj_quaternaryLabelColor;
@end

@implementation MJDynamicColor
...此部分代碼省略,都是類似下面代碼重寫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

這樣有兩個好處:一是懶加載使得性能提高了;二是多個地方使用相同的顏色時更方便統(tǒng)一修改。

使用起來如下:

label.textColor = MJCOLOR.mj_labelColor;

以上使用方式都是創(chuàng)建動態(tài)顏色,也就是自定義的動態(tài)顏色,蘋果的API也提供了官方的動態(tài)顏色,也稱為語義顏色,直接使用就可以,在UIInterface.h文件中可以看到。

圖片適配

圖片適配和顏色適配類似,也有動態(tài)圖片的概念,通過XCode創(chuàng)建,在.xcassets文件中把圖片改成動態(tài)圖片就行:

image.png

使用處的代碼不用修改,還是通過imageNamed方法獲取。這個是iOS13之前的方法,所以不用判斷系統(tǒng)版本

[self.leftCloseButton setImage:[UIImage imageNamed:@"feeds_back_white"]

在夜間模式下如果重新使用一張圖片,會使得圖片資源大小翻倍,所以一般都是加一層遮罩,特定情況下才使用新圖片。這種情況有種偷懶的方法:

#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添加一個分類,重寫traitCollectionDidChange方法,這個方法在traitCollection更改時會調(diào)用,這時候加一個遮罩就可以了。重寫了didMoveToWindow方法的原因是,在Dark模式下啟動APP,不會顯示Dark模式(夜間模式)的外觀。因為traitCollectionDidChange沒有調(diào)用,這個方法在traitCollection更改的時候才會調(diào)用。

總結(jié):

編寫Demo時發(fā)現(xiàn)的很費時間的點,也發(fā)現(xiàn)給圖片或者顏色統(tǒng)一做處理行不通,夜間模式下的每個頁面都需要UI重新設(shè)計,開發(fā)也需要重新聯(lián)調(diào)每個頁面,需要的時間非常漫長。

  1. 在顏色適配中,每個夜間(Dark)模式下給的顏色都要UI重新設(shè)計,并和開發(fā)聯(lián)調(diào),因為夜間模式下,不能隨便給一個對應(yīng)顏色,使用蘋果官方提供的動態(tài)顏色效果也差(相當(dāng)于開發(fā)人員自己來設(shè)計UI,并反復(fù)調(diào)試效果)。所以需要給APP所有頁面重新設(shè)計一套夜間模式下的UI。

  2. 圖片適配中,單一處理行不通,也需要每個頁面單獨過UI如:

    1. 統(tǒng)一添加遮罩:本身就是起遮罩作用的UIImageView。
    2. 統(tǒng)一添加遮罩:很多圖片的外邊緣部分是透明的,加遮罩后外邊緣不透明了,顯示出邊緣部分了。
    3. 統(tǒng)一添加遮罩:有些圖片會動態(tài)修改大小,但是遮罩不會跟隨著變動。
    4. 給定新圖片 :新圖片也需要UI重新設(shè)計的時間,因為要保證和頁面其它部分協(xié)調(diào),直接給定一張單一方式處理的圖片,如只是改了下圖片的亮度,很可能跟頁面不協(xié)調(diào)。

折中方案:

  1. 只修改背景色,圖片和文字在用戶能正常使用的情況下都不做處理,我看微信在夜間模式下的效果基本就是這樣。
  2. 整個APP不支持暗黑模式,因為蘋果官方并沒有在APP Store Review Guidelines中規(guī)定某些類型APP必須兼容暗黑模式,某些不需要兼容,而是沒有提到相關(guān)內(nèi)容。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最近公司業(yè)務(wù)需求要更換APP主題。最開始是一個地方一個地方去改,而且項目中很多老代碼是用xib寫的,習(xí)慣純代碼編程...
    抓魚貓L閱讀 6,445評論 0 10
  • 目錄 1.適配暗黑模式(Dark Mode)1.1顏色適配* 系統(tǒng)動態(tài)顏色** 自定義動態(tài)UIColor(代碼自定...
    冰點雨閱讀 3,381評論 1 11
  • iOS 13終于引來了暗黑模式。 每當(dāng)新特性的到來,iOS開發(fā)者們既緊張又有點小興奮,懷揣著被虐的心態(tài),讓我們來看...
    koin447閱讀 64,241評論 16 106
  • 掙扎中的拖延著:成為戰(zhàn)敗者怎么辦 爭奪控制權(quán)的較量 對我們每個人來說,對自己的生活具有一定的掌控感是十分重要的一件...
    Dl_毛良偉閱讀 226評論 0 1
  • 嬌蘭花開一二朵 吐蕊繞蝶三四月 鐘情已有五六載 相思七八里 九十是歸期
    蘭妤妤閱讀 246評論 0 1

友情鏈接更多精彩內(nèi)容