iOS:Dark Mode-暗黑模式调研

背景

iOS 13苹果公司推出了暗黑模式,APP默认支持,用户可以通过在设置-显示与亮度-外观栏中选择深色来打开暗黑模式,但是,如果开发工程师不进行适配,应用内可能会出现某些视图的颜色变成黑色,影响显示效果。

要防止这种情况可以给控制器或者视图设置overrideUserInterfaceStyle属性为UIUserInterfaceStyleLight或者UIUserInterfaceStyleDark,这样当前视图和它的所有子视图都会固定为Dark或者Light模式。也可以在info.plist中加入UIUserInterfaceStyle键,给定Light值,使整个应用忽略暗黑模式。

苹果公司在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,你可以学习如何退出。

同时,适配暗黑模式是强烈建议的,仅在适配暗黑模式的过程中,使用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对象记录界面环境特征,里面包含Size Class,Layout Direction,User Interface Style信息(Dark或者Light)。每个UIView,UIViewController和UIPresentationController对象都持有这个对象。子视图被添加到父视图的时候,子视图会继承父视图的UITraitCollection,UITraitCollection信息就从UIScreen一直传递到当前显示的UIView:UIScreen->UIWindow->UIPresentationViewController->UIViewController→UIView。

用户更改了系统外观后,系统通过调用以下方法重新渲染视图,完成系统外观的切换:

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

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

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

在这些方法调用前,系统会更新UITraitCollection对象,所以要在这些方法中加入Dark模式和Light模式有区别的代码,如Dark模式下要在图片上加一层遮罩,Light则要隐藏。如果写在别的地方,如在初始化方法或者viewDidLoad中,会造成模式切换后,遮罩还在,或者一直不显示。

适配

在适配实践中会总结出更好的实现方式,或者发现很多细节需要处理,这些都会影响开发时间。所以调研时编写Demo并根据实际项目调试效果是很有必要的。

颜色适配

颜色适配只要将UIColor对象改成动态颜色对象即可。动态颜色对象在不同的外观下,有不同的颜色值。它也是UIColor对象,但是创建的方式不一样。UIKit会根据UITraitCollection信息解析出对应外观的颜色值。具体使用如下:

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是创建动态颜色的方法。resolvedColorWithTraitCollection是把动态颜色解析成固定颜色的方法,在创建动态颜色的block中不能返回动态颜色,这里在Dark模式下使用了系统的secondarySystemBackgroundColor动态颜色,所以返回时做了解析。

动态颜色也可以通过Xcode创建,步骤如下:

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]
//所有动态颜色获取的地方,适配暗黑模式
@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的文字的颜色
/// 类似一级标题
@property (nonatomic, strong) UIColor *mj_labelColor;
/// 类似二级标题
@property (nonatomic, strong) UIColor *mj_secondaryLabelColor;
/// 类似三级标题
@property (nonatomic, strong) UIColor *mj_tertiaryLabelColor;
/// 类似四级标题
@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

这样有两个好处:一是懒加载使得性能提高了;二是多个地方使用相同的颜色时更方便统一修改。

使用起来如下:

label.textColor = MJCOLOR.mj_labelColor;

以上使用方式都是创建动态颜色,也就是自定义的动态颜色,苹果的API也提供了官方的动态颜色,也称为语义颜色,直接使用就可以,在UIInterface.h文件中可以看到。

图片适配

图片适配和颜色适配类似,也有动态图片的概念,通过XCode创建,在.xcassets文件中把图片改成动态图片就行:

image.png

使用处的代码不用修改,还是通过imageNamed方法获取。这个是iOS13之前的方法,所以不用判断系统版本

[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更改时会调用,这时候加一个遮罩就可以了。重写了didMoveToWindow方法的原因是,在Dark模式下启动APP,不会显示Dark模式(夜间模式)的外观。因为traitCollectionDidChange没有调用,这个方法在traitCollection更改的时候才会调用。

总结:

编写Demo时发现的很费时间的点,也发现给图片或者颜色统一做处理行不通,夜间模式下的每个页面都需要UI重新设计,开发也需要重新联调每个页面,需要的时间非常漫长。

  1. 在颜色适配中,每个夜间(Dark)模式下给的颜色都要UI重新设计,并和开发联调,因为夜间模式下,不能随便给一个对应颜色,使用苹果官方提供的动态颜色效果也差(相当于开发人员自己来设计UI,并反复调试效果)。所以需要给APP所有页面重新设计一套夜间模式下的UI。

  2. 图片适配中,单一处理行不通,也需要每个页面单独过UI如:

    1. 统一添加遮罩:本身就是起遮罩作用的UIImageView。
    2. 统一添加遮罩:很多图片的外边缘部分是透明的,加遮罩后外边缘不透明了,显示出边缘部分了。
    3. 统一添加遮罩:有些图片会动态修改大小,但是遮罩不会跟随着变动。
    4. 给定新图片 :新图片也需要UI重新设计的时间,因为要保证和页面其它部分协调,直接给定一张单一方式处理的图片,如只是改了下图片的亮度,很可能跟页面不协调。

折中方案:

  1. 只修改背景色,图片和文字在用户能正常使用的情况下都不做处理,我看微信在夜间模式下的效果基本就是这样。
  2. 整个APP不支持暗黑模式,因为苹果官方并没有在APP Store Review Guidelines中规定某些类型APP必须兼容暗黑模式,某些不需要兼容,而是没有提到相关内容。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • 最近公司业务需求要更换APP主题。最开始是一个地方一个地方去改,而且项目中很多老代码是用xib写的,习惯纯代码编程...
    抓鱼猫L阅读 6,150评论 0 10
  • 目录 1.适配暗黑模式(Dark Mode)1.1颜色适配* 系统动态颜色** 自定义动态UIColor(代码自定...
    冰点雨阅读 3,065评论 1 11
  • iOS 13终于引来了暗黑模式。 每当新特性的到来,iOS开发者们既紧张又有点小兴奋,怀揣着被虐的心态,让我们来看...
    koin447阅读 63,662评论 16 106
  • 挣扎中的拖延着:成为战败者怎么办 争夺控制权的较量 对我们每个人来说,对自己的生活具有一定的掌控感是十分重要的一件...
    Dl_毛良伟阅读 194评论 0 1
  • 娇兰花开一二朵 吐蕊绕蝶三四月 钟情已有五六载 相思七八里 九十是归期
    兰妤妤阅读 203评论 0 1