关于iOS13 DarkMode的适配那些事儿

一、为什么要调研这个暗黑模式的适配?

在2020年3月4日,苹果粑粑可能心情大好发了这么一个公告:


image.png

在这个公告里面,苹果提了三个要求:
1.至2020年4月30日起,开发者必须使用iOS1或更高版本打包才能提交到App Store
2.至2020年4月30日起,开发者必使用storyboard来提供应用的launchScreen
3.至2020年4月30日起,开发者需要给自己的应用完成所有屏幕尺寸的适配

公告中提到了Dark Mode,但是并没有明确表示需要在2020年4月30日之前完成Dark Mode的适配,所以关于网上那些说不适配Dark Mode会导致应用下架的完全是无稽之谈,那么可能很多童鞋又会问Apple是不是支持开发者做这个Dark Mode,答案是肯定的,苹果为了Dark Mode做了很多工作,比如说提供了一些接口、各种适配指南等,但是这种支持只是“强烈建议”还远远没有上升到不支持Dark Mode就要下架的程度。
个人认为,在遥远的未来,可能苹果粑粑真的会要求开发者要对自己的 应用做Dark Mode的适配,话不多说,我们接下来就来看看怎么来做这个暗黑模式(Dark Mode)的适配?

二、Dark Mode有哪些适配方案,各自的优缺点是什么?

对于广大iOS开发者来说,适配Dark Mode并不像设置语言那么简单,设置语言之后,手机会重启,即便是开发者自己实现的语言切换的功能 切换语言也会让App重新初始化,相反Dark Mode是要求在切换主题之后,App在运行状态中去更新配色和素材,这也是适配暗黑模式的难点所在,而且对于开发者来说,工作量无疑是巨大的。

苹果提供的适配方案主要有两个:

1、将两种主题不同的素材直接存储在对象中,UIKit在主题变化时获取对应的素材更新展示。

优点:工作量相对较少,对开发者比较友好
缺点:灵活性差

2、给出主题变化的通知,让开发者在主题变化的通知回调里面做相应的适配工作。

优点:高度自定义,灵活性非常强
缺点:适配工作工作量巨大
对于不同的适配方案,开发者需要根据应用的实际情况去选择相应的适配方案,下面分别针对两种方案做一下实际适配:

1.1 颜色的适配
1.1.1 使用UIKIt提供的动态颜色
- (UILabel *)textLabel {
    if (!_textLabel) {
        _textLabel = [[UILabel alloc] initWithFrame:CGRectMake(37, 50, kLabelWidth, kLabelHeight)];
        _textLabel.backgroundColor = [UIColor lightGrayColor];
        _textLabel.layer.cornerRadius = 30.0;
        _textLabel.clipsToBounds = YES;
        _textLabel.text = @"使用UIKit提供的动态颜色";
        _textLabel.textAlignment = NSTextAlignmentCenter;
        if (@available(iOS 13.0, *)) {
            _textLabel.textColor  = UIColor.secondaryLabelColor;
        }else {
            _textLabel.textColor = [UIColor redColor];
        }
        _textLabel.font = [UIFont systemFontOfSize:16.0 weight:UIFontWeightMedium];
    }
    return _textLabel;
}

secondaryLabelColor、labelColor等这些颜色都内置了对两套主题的适配,在iOS13下,UIKit 提供的视图组件的背景颜色、文字颜色等属性都是适配过两种主题的颜色,创建的视图组件即使没有手动设置颜色,也是已经适配过两种主题的。

1.1.2 创建动态颜色
- (UILabel *)textLabel1 {
    if (!_textLabel1) {
        _textLabel1 = [[UILabel alloc] initWithFrame:CGRectMake(37, 50 + kLabelHeight + 20, kLabelWidth, kLabelHeight)];
        _textLabel1.backgroundColor = [UIColor lightGrayColor];
        _textLabel1.layer.cornerRadius = 30.0;
        _textLabel1.clipsToBounds = YES;
        _textLabel1.text = @"动态的创建颜色";
        _textLabel1.textAlignment = NSTextAlignmentCenter;
        if (@available(iOS 13.0, *)) {
            _textLabel1.textColor  = [UIColor ww_colorWithLightColor:[UIColor redColor] darkColor:[UIColor greenColor]];
        }else {
            _textLabel1.textColor = [UIColor redColor];
        }
        _textLabel1.font = [UIFont systemFontOfSize:16.0 weight:UIFontWeightMedium];
    }
    return _textLabel1;
}

/// 动态更换颜色的具体实现
+ (UIColor *)ww_colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
#if __IPHONE_13_0
    if (@available(iOS 13.0, *)) {
        return [self colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
                switch (traitCollection.userInterfaceStyle) {
                    case UIUserInterfaceStyleDark:
                        return darkColor ?: lightColor;
                    case UIUserInterfaceStyleLight:
                    case UIUserInterfaceStyleUnspecified:
                    default:
                        return lightColor;
                }
            }];
        } else {
#endif
        return lightColor;
#if __IPHONE_13_0
        }
#endif
}
1.1.3 在assets里面添加动态颜色资源

在Xcode11里面可以把颜色当作一种资源添加到assets里面,一个颜色组可以有多个颜色,适配不同的主题模式。


image.png
- (UILabel *)textLabel2 {
    if (!_textLabel2) {
        _textLabel2 = [[UILabel alloc] initWithFrame:CGRectMake(37, 50 + kLabelHeight *2 + 20 *2, kLabelWidth, kLabelHeight)];
        _textLabel2.backgroundColor = [UIColor lightGrayColor];
        _textLabel2.layer.cornerRadius = 30.0;
        _textLabel2.clipsToBounds = YES;
        _textLabel2.text = @"在Assets中添加动态资源";
        _textLabel2.textAlignment = NSTextAlignmentCenter;
        if (@available(iOS 13.0, *)) {
            _textLabel2.textColor  = [UIColor colorNamed:@"labelColor"];
        }else {
            _textLabel2.textColor = [UIColor redColor];
        }
        _textLabel2.font = [UIFont systemFontOfSize:16.0 weight:UIFontWeightMedium];
    }
    return _textLabel2;
}
1.2 图片的适配
1.2.1 在assets中添加动态图片资源
image.png

在Xcode11里面,assets里面的一张图片除了根据scale分成三张外,还要根据主题分成三组,如果再根据是否是高对比度、颜色色域、布局方向,这么算下来配置一张图片就需要:33222 = 72张图片资源。

- (UIImageView *)imageView1 {
    if (!_imageView1) {
        _imageView1 = [[UIImageView alloc] initWithFrame:CGRectMake(150, 50 + kLabelHeight *3 + 20 *3, kSmallImageViewWH, kSmallImageViewWH)];
        _imageView1.image = [UIImage imageNamed:@"image"];
    }
    return _imageView1;
}
1.2.2 创建自己的动态图片
- (UIImageView *)imageView2 {
    if (!_imageView2) {
        _imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(196, 50 + kLabelHeight *3 + 20 *3, kSmallImageViewWH, kSmallImageViewWH)];
        _imageView2.image = [UIImage ww_imageWithLightImage:[UIImage imageNamed:@"image2_light"] darkImage:[UIImage imageNamed:@"image2_dark"]];
    }
    return _imageView2;
}

/// 创建动态图片的具体实现
+ (UIImage *)ww_imageWithLightImage:(UIImage *)lightImage darkImage:(UIImage *)darkImage {
    if (!lightImage) {
        return nil;
    }
#if __IPHONE_13_0
    if (@available(iOS 13.0, *)) {
        UITraitCollection *lightCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight];
        UITraitCollection *darkCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
        UITraitCollection *unspecifiedCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight];
        UIImage *image = UIImage.new;
        UIImage *darkPure = [darkImage.imageAsset imageWithTraitCollection:unspecifiedCollection];
        UIImage *lightPure = [lightImage.imageAsset imageWithTraitCollection:unspecifiedCollection];
        [image.imageAsset registerImage:lightPure withTraitCollection:lightCollection];
        [image.imageAsset registerImage:darkPure withTraitCollection:darkCollection];
        [image.imageAsset registerImage:lightPure withTraitCollection:unspecifiedCollection];
        return image;
    } else {
#endif
        return lightImage;
#if __IPHONE_13_0
    }
#endif
}
1.2.3 网络图片

比如有这么一种场景:我浅色主题和深色主题下显示不同的图片,并且两张图片都不是本地的,都是从网络获取,实现思路大概是:先把两张图片下下来,在本地组装成动态图片

- (UIImageView *)imageView3 {
    if (!_imageView3) {
        _imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(45, 50 + kLabelHeight *3 + 20 *4 + kSmallImageViewWH, 280, kImageView3H)];
        _imageView3.backgroundColor = [UIColor lightGrayColor];
        _imageView3.layer.cornerRadius = 10.0;
        _imageView3.clipsToBounds = YES;
        
    }
    return _imageView3;
}

/// 具体实现方案
+ (void)ww_imageWithLightUrl:(NSURL *)lightUrl darkUrl:(NSURL * _Nullable)darkUrl completion:(void(^)(UIImage * _Nullable image, NSError * _Nullable error))completion {
    __block BOOL darkFinish = false;
    __block BOOL lightFinish = false;
    __block UIImage *lightImage;
    __block UIImage *darkImage;
    __block NSError *downloadError;
    void(^finishBlock)(void) = ^() {
        if (lightImage && darkImage) {
            UIImage *image = [UIImage ww_imageWithLightImage:lightImage darkImage:darkImage];
            completion(image, nil);
            return;
        }
        NSError *noImageError = [NSError errorWithDomain:@"com.WWDarkModeDemo.Remote" code:0 userInfo:@{@"message": @"图片为nil,请检查你的图片."}];
        completion(nil, noImageError);
       };
       finishBlock = [finishBlock copy];
    
       if (lightUrl == nil || darkUrl == nil) {
            NSError *error = [[NSError alloc] initWithDomain:@"com.WWDarkModeDemo.Extension" code:0 userInfo:@{@"message":@"浅色主题或深色主题的url不能为nil."}];
            completion(nil,error);
        }
    
      [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:lightUrl completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
          lightFinish = YES;
          downloadError = error;
          lightImage = image;
          finishBlock();
      }];
    
      [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:darkUrl completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
           darkFinish = YES;
           downloadError = error;
           darkImage = image;
           finishBlock();
      }];
}
2.1 UITraitCollection是什么?

UITraitCollection是用来处理苹果手机的一些特性的存储和UI相关的配置 比如我修改了某些系统设置,如:改字体大小

+ (UITraitCollection *)traitCollectionWithPreferredContentSizeCategory:(UIContentSizeCategory)preferredContentSizeCategory API_AVAILABLE(ios(10.0));
@property (nonatomic, copy, readonly) UIContentSizeCategory preferredContentSizeCategory API_AVAILABLE(ios(10.0)); // unspecified: UIContentSizeCategoryUnspecified

关于主题模式切换的属性是我们本节关注的重点:

typedef NS_ENUM(NSInteger, UIUserInterfaceStyle) {
    UIUserInterfaceStyleUnspecified,
    UIUserInterfaceStyleLight,
    UIUserInterfaceStyleDark,
} API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);

+ (UITraitCollection *)traitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)userInterfaceStyle API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);
@property (nonatomic, readonly) UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos); // unspecified: UIUserInterfaceStyleUnspecified
2.2 通过子类重写traitCollectionDidChange这个方法来监听系统设置的一些属性变化
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    [super traitCollectionDidChange:previousTraitCollection];
//    NSLog(@"previousTraitCollection - %ld \n self.traitCollection - %ld",previousTraitCollection.userInterfaceStyle,self.traitCollection.userInterfaceStyle);
    if (@available(iOS 13.0, *)) {
        if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
            if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                self.textLabel.textColor = [UIColor orangeColor];
            }else {
                self.textLabel.textColor = [UIColor yellowColor];
            }
        }
    }
    
//    UIColor *labelTextColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
//        if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
//            return [UIColor orangeColor];
//        } else {
//            return [UIColor yellowColor];
//        }
//    }];
//
//    self.textLabel.textColor = labelTextColor;
}

三、Q&A

Q:如何让应用不支持暗黑模式?
A:info.plist里面添加属性User Interface Stylelight

Q:如何让应用的某些页面不支持暗黑模式?
A:

// 设置当前页面的主题模式 就不跟随系统设置模式的变化而变化
 self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;

Q:系统主题更换之后会调用哪些方法?
A:
在ViewController:
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
- (void)updateViewConstraints
- (void)viewWillLayoutSubviews
- (void)viewDidLayoutSubviews

在View里:
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
- (void)drawRect:(CGRect)rect
- (void)layoutSubviews
- (void)updateConstraints
- (void)tintColorDidChange

在UIPresentationController里:
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
- (void)containerViewWillLayoutSubviews
- (void)containerViewDidLayoutSubviews

四、结尾

最好的资料在官方文档:

https://developer.apple.com/documentation/xcode/supporting_dark_mode_in_your_interface?changes=latest_minor

WWDC视频:https://developer.apple.com/videos/play/wwdc2019/214

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容