写在前面的话
在WWDC2019的时候,我们的尊敬的库克爵士(蒂莫西·唐纳德·库克)公布了iOS13.0系统的升级,其中包括了照片编辑,新的“High-Key Mono”光效,一些常用应用程序的功能性升级,比如地图,提醒,记事...还有我们最重要的深色模式!
>> Implementing Dark Mode on iOS <<
不对,错了,是这个图。
咳咳咳,总之和Android Q一样,iOS系统现在也可以进行深色模式的切换了,真是可喜可贺,可喜可贺...
然而,真的是这样甜这样简单吗?
我的APP能一键随着系统切换深色模式吗?
我需要书写很多繁重冗余的代码吗?
我的图片会变得怎样,我的图标该怎么办?
我需要进行小细节上的适配吗?
(此处唐僧)...等等,这都是开发者需要关注的问题。
由于笔者最近在做业务需求的时候分配到了这个任务,而且,一开始觉得很简单的东西:差不多工作量1周(?)的FLAG,
直到越深挖越感叹这个坑如此之大。
因此,写出这篇较为全面的深色模式全面适配指南来与各位分享。
容易被混淆的概念
深色模式(Dark Mode),夜间模式(Night Mode),颜色反转(Inverted Colors),这几个词都代表了一些在昏暗环境下能控制贴合人眼浏览体验的效果,那么他们是不是基本类似的?
深色模式 ≠ 夜间模式
深色模式 ≠ 颜色反转
前期计划
决定配色方案,瞄准灰度层级
彩色该怎么办呢
比起上面的灰度层级,彩色部分更好控制,它们几乎不需要在色彩空间中被反转。因此,在这里的建议做法是直接调整它们的饱和度,稍微降低一些即可。因为饱和度较高的颜色容易在深色背景下更为显得刺眼,引起视觉上的疲劳。
在深色背景下观察一下各个彩色是否符合预期的显示效果。
整合配色方案,语义化
我们在上面完成了基本的配色方案,现在总算可以把我们新增加的内容整合在一起啦。
以上就是我们这次所订制出来新的配色方案。
特别需要注意的是,每个颜色的左边都有其相对应的名字,我们使用
- colorGrayLighter;
- colorGrayLight;
- colorGrayNormal...
等等来进行各级灰度的层次划分,这是颜色名的语义化标签。
当然,你也可以使用
- colorGray1;
- colorGray2;
- colorGray3...
等较为简单的命名风格来进行划分(Apple就是这么做的,你可以在 UIColor 中找到系统的配色方案)。
但是这里不太推崇这样的方法,来进行颜色的硬性指定
- colorTitle;
- colorSubtitle;
- colorText...
我了解这样做的理由,比如这样的话,作为标题显示的 UILabel 就可以直接使用colorTitle了,的确比较方便管理。
用但是,今后的业务复杂度不太好预测,比如一个custom的 UITableViewCell 中,可能我们含有主标题(title),副标题(subtitle),正文(text),但是,如果今后还有装饰性文字(widget text),或者序号(number),那么强行套用以上的颜色名显然是不合理的,扩充颜色库也会造成不小的麻烦。
所以,这里的语义化(semantic)不应当依附于功能(function),这是我个人的一点看法。
从可视化到代码
现在,我们已经有我们应用程序的配色方案咯。
我们需要将其还原为代码,首先从封装一个简单的类开始吧。
可以在这里下载到Demo工程:
(OC)DarkModeDemoOC
(Swift)DarkModeDemoSwift
UIColor是动态颜色
iOS 13 之前 UIColor 只能表示一种颜色,从 iOS 13 开始 UIColor 是一个动态的颜色,它可以在 LightMode 和 DarkMode 拥有不同的颜色。
UIColor增加了新的初始化方法(OC)colorWithDynamicProvider:与(Swfit)UIColor.init(dynamicProvider:),可以将闭包传入做一些基本的逻辑判断。
Objective-c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+ (UIColor*)colorWithDarkMode:(UIColor *)darkColor lightColor:(UIColor *)lightColor{ UIColor* color = nil; if(@available(iOS 13.0,*)){ color = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection){ if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark){ return darkColor; } else{ return lightColor; } }]; } else{ color = lightColor; } return color; } |
Swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
static func colorWithDarkMode(darkColor:UIColor,lightColor:UIColor)->UIColor{ var color:UIColor? = nil if #available(iOS 13.0, *){ color = UIColor.init(dynamicProvider: { (traitCollection) -> UIColor in if(traitCollection.userInterfaceStyle == .dark){ return darkColor } else{ return lightColor } }) } else{ color = lightColor } return color! } |
简单的配色方案类
在我们的应用程序的工程目录中新建对应的类文件,命名为 AppColorScheme,我们将在这里书写代码。
我们需要罗列出配色方案中的各种颜色。
Objective-c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@interface APPColorScheme:NSObject + (UIColor*)colorWithDarkMode:(UIColor*)darkColor lightColor:(UIColor*)lightColor; @property(nonatomic,readonly,class) UIColor* colorBlue; @property(nonatomic,readonly,class) UIColor* colorOrange; @property(nonatomic,readonly,class) UIColor* colorGreen; @property(nonatomic,readonly,class) UIColor* colorWhite; @property(nonatomic,readonly,class) UIColor* colorGrayLighter; @property(nonatomic,readonly,class) UIColor* colorGrayLight; @property(nonatomic,readonly,class) UIColor* colorGrayNormal; @property(nonatomic,readonly,class) UIColor* colorGrayHeavy; @property(nonatomic,readonly,class) UIColor* colorGrayHeavier; @property(nonatomic,readonly,class) UIColor* colorBlack; @property(nonatomic,readonly,class) UIColor* colorForeground; @property(nonatomic,readonly,class) UIColor* colorBackground; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
@implementation APPColorScheme + (UIColor*)colorWithDarkMode:(UIColor *)darkColor lightColor:(UIColor *)lightColor{ UIColor* color = nil; if(@available(iOS 13.0,*)){ color = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection){ if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark){ return darkColor; } else{ return lightColor; } }]; } else{ color = lightColor; } return color; } + (UIColor*)colorBlue{ return [self colorWithDarkMode:[UIColor colorWithHex:0x429fde] lightColor:[UIColor colorWithHex:0x0097ff]]; } + (UIColor*)colorOrange{ return [self colorWithDarkMode:[UIColor colorWithHex:0xce6647] lightColor:[UIColor colorWithHex:0xff7850]]; } + (UIColor*)colorGreen{ return [self colorWithDarkMode:[UIColor colorWithHex:0xb9ff50] lightColor:[UIColor colorWithHex:0x9aca52]]; } + (UIColor*)colorWhite{ return [self colorWithDarkMode:[UIColor colorWithHex:0x000000] lightColor:[UIColor colorWithHex:0xffffff]]; } + (UIColor*)colorGrayLighter{ return [self colorWithDarkMode:[UIColor colorWithHex:0x262626] lightColor:[UIColor colorWithHex:0xe5e5e5]]; } + (UIColor*)colorGrayLight{ return [self colorWithDarkMode:[UIColor colorWithHex:0x3a3a3c] lightColor:[UIColor colorWithHex:0xcccccc]]; } + (UIColor*)colorGrayNormal{ return [self colorWithDarkMode:[UIColor colorWithHex:0x666666] lightColor:[UIColor colorWithHex:0x999999]]; } + (UIColor*)colorGrayHeavy{ return [self colorWithDarkMode:[UIColor colorWithHex:0x999999] lightColor:[UIColor colorWithHex:0x666666]]; } + (UIColor*)colorGrayHeavier{ return [self colorWithDarkMode:[UIColor colorWithHex:0xcccccc] lightColor:[UIColor colorWithHex:0x333333]]; } + (UIColor*)colorBlack{ return [self colorWithDarkMode:[UIColor colorWithHex:0xffffff] lightColor:[UIColor colorWithHex:0x000000]]; } + (UIColor*)colorForeground{ return [self colorWithDarkMode:[UIColor colorWithHex:0x1a1a1a] lightColor:[UIColor colorWithHex:0xffffff]]; } + (UIColor*)colorBackground{ return [self colorWithDarkMode:[UIColor colorWithHex:0x000000] lightColor:[UIColor colorWithHex:0xf7f7f7]]; } @end |
Swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
class APPColorScheme{ static func colorWithDarkMode(darkColor:UIColor,lightColor:UIColor)->UIColor{ var color:UIColor? = nil if #available(iOS 13.0, *){ color = UIColor.init(dynamicProvider: { (traitCollection) -> UIColor in if(traitCollection.userInterfaceStyle == .dark){ return darkColor } else{ return lightColor } }) } else{ color = lightColor } return color! } static var colorBlue:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x429fde), lightColor: UIColor(rgb: 0x0097ff)) } static var colorOrange:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0xce6647), lightColor: UIColor(rgb: 0xff7850)) } static var colorGreen:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x9aca52), lightColor: UIColor(rgb: 0xb9ff50)) } static var colorWhite:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x000000), lightColor: UIColor(rgb: 0xffffff)) } static var colorGrayLighter:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x262626), lightColor: UIColor(rgb: 0xe5e5e5)) } static var colorGrayLight:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x3a3a3c), lightColor: UIColor(rgb: 0xcccccc)) } static var colorGrayNormal:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x666666), lightColor: UIColor(rgb: 0x999999)) } static var colorGrayHeavy:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x999999), lightColor: UIColor(rgb: 0x666666)) } static var colorGrayHeavier:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0xcccccc), lightColor: UIColor(rgb: 0x333333)) } static var colorBlack:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0xffffff), lightColor: UIColor(rgb: 0x000000)) } static var colorForeground:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x1a1a1a), lightColor: UIColor(rgb: 0xffffff)) } static var colorBackground:UIColor{ return self.colorWithDarkMode(darkColor: UIColor(rgb: 0x000000), lightColor: UIColor(rgb: 0xf7f7f7)) } } |
我们先参照配色方案,在 APPColorScheme 中申明颜色的静态属性,以及 colorWithDarkMode: 方法,以供各个 UIViewController 使用。
使用方法
有了我们的配色方案类,我们就可以使用它们了,我们可以这样写
Objective-c
1 2 3 |
UILabel* titleLabel = [[UILabel alloc] init]; titleLabel.text = @"深色模式文字在这里"; titleLabel.textColor = APPColorScheme.colorGrayHeavier; |
Swift
1 2 3 |
let titleLabel = UILabel(); titleLabel.text = "深色模式文字在这里"; titleLabel.textColor = APPColorScheme.colorGrayHeavier; |
是不是很简单?仅仅替换各个控件的颜色代码就可以。在系统切换深色模式/亮色模式之后,控件的颜色也会随之相应改变。
替换硬编写的颜色代码
所以,深色模式的适配是一件很简单的事情,只要将对应的颜色代码替换就行了。
我们只需要参考配色方案表,将原来的颜色代码替换成 APPColorScheme.colorGrayHeavier 这样的形式。
但是,在我们现在的工程项目中,各个控件的颜色设置可能有这几种情况。
- 使用UIColor的系统内置颜色(如systemGray1,labelColor等);
- 使用形如UIColor(red: 0.3, green: 0.4, blue: 0.5, alpha: 0.7)等使用RGB作为参数生成的颜色;
- 使用形如[UIColor colorWithHex:0x429fde]等自定义方法,配合16进制颜色作为参数生成的颜色。
我们的坏消息是:以上3种情况无一例坏地都需要手动进行颜色替换。
对于较大规模的应用程序项目工程中,可能有有数千处,甚至上万处包含以上的颜色代码。没有什么捷径,一点一点进行替换吧。
一些通用方法
上面我们提到的,colorWithDarkMode: 是一个通用方法,对于特殊情况,可以在外部使用它。
比如,
设计师:我希望这个文字在亮色模式下是橘色的,在暗色模式下是灰色的
工程师:...? 说好的自动按配色方案来呢?
设计师:这样比较好看,特殊情况特殊处理嘛,组织上已经决定了
工程师:得令~
那我们就可以这样写
1 2 3 |
UILabel* titleLabel = [[UILabel alloc] init]; titleLabel.text = @"深色模式文字在这里"; titleLabel.textColor = [APPColorScheme colorWithDarkMode:APPColorScheme.colorGrayNormal lightColor:APPColorScheme.colorOrange]; |
注意,这样的特例最好不要太多,以后维护起来会变的越来越麻烦。
不起作用的CGColor
很遗憾,CGColor并没有提供对应的功能来自动切换颜色,这种情况常见于为layer.borderColor设置颜色时。
如果设置了:
titleLabel.layer.borderColor = APPColorScheme.colorGrayLighter.CGColor;
在第一次启动APP时,会进行正确的颜色设置,然而,如果在APP处于后台时,在系统设置中进行深色 / 亮色模式的切换的话,是不会自动切换到对应的显示效果的。
在这里的解决方案为如下所示。
1 2 3 4 5 |
- (void) traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{ [super traitCollectionDidChange:previousTraitCollection]; titleLabel.layer.borderColor = MHColorScheme.colorGrayLighter.CGColor; } |
在检测到显示模式切换时,重新设置一下对应控件的颜色即可。
替换图片
图片的适配更为简单,请提供对应亮色 / 深色模式下显示的图片即可。
首先找到对应xcasset中的图片。
选中,将其右侧属性栏中的Apperance从默认的None设置为Dark/Any。
让我们来看看效果图吧。
Bingo!
这就是一个适配DarkMode的简单方法。
深色模式的适配方法较为简单,但是对于一个已经存在上线的项目工程来说,工作量无疑是巨大的。
需要有足够的耐心,与...人参时间。
但是,请慢慢来吧~
期待与你下次再见。