一、为什么要调研这个暗黑模式的适配?
在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 Style
为light
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
四、结尾
最好的资料在官方文档:
网友评论