首先我们来看下效果图
DarkMode 主要从两个方面来适配,一是颜色,二是图片,适配的代码不是很多,接下来让我们一起来看看具体是怎么操作的吧。
iOS 13 之前 UIColor
只能表示一种颜色,从 iOS 13 开始 UIColor
是一个动态的颜色,它可以在 LightMode 和 DarkMode 拥有不同的颜色。
iOS 13 下 UIColor
增加了很多动态颜色,我们来看下用系统提供的颜色能实现怎么样的效果。
@property (class, nonatomic, readonly) UIColor *systemBrownColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *systemIndigoColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *systemGray2Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *systemGray3Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *systemGray4Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *systemGray5Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *systemGray6Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *labelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *secondaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *tertiaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *quaternaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *linkColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *placeholderTextColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *separatorColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *opaqueSeparatorColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (class, nonatomic, readonly) UIColor *systemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *secondarySystemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *tertiarySystemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *systemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *secondarySystemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *tertiarySystemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *systemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *secondarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *tertiarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos); @property (class, nonatomic, readonly) UIColor *quaternarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
实例[self.view setBackgroundColor:[UIColor systemBackgroundColor]]; [self.titleLabel setTextColor:[UIColor labelColor]]; [self.detailLabel setTextColor:[UIColor placeholderTextColor]];
复制代码
怎么样,看起来和 iOS 13 之前设置一个颜色的方法一样吧,用这种动态颜色,系统直接替我们完成了适配的工作,是不是很方便呢。
如何自己创建一个动态的 UIColor
上面我们说到系统提供了一些动态的颜色供我们使用,但是在正常开发中,系统提供的颜色肯定是不够用的,所以我们要自己创建动态颜色。
iOS 13 下 UIColor
增加了一个初始化方法,我们可以用这个初始化方法来创建动态颜色。
+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
- (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
这个方法要求传一个闭包进去,当系统从 LightMode 和 DarkMode 之间切换的时候就会触发这个回调。
这个闭包返回一个 UITraitCollection
类,我们要用这个类的 userInterfaceStyle
属性。userInterfaceStyle
是一个枚举,声明如下
typedef NS_ENUM(NSInteger, UIUserInterfaceStyle) {
UIUserInterfaceStyleUnspecified,
UIUserInterfaceStyleLight,
UIUserInterfaceStyleDark,
} API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);
这个枚举会告诉我们当前是 LightMode or DarkMode
现在我们创建两个 UIColor
并赋值给 view.backgroundColor
和 label
,代码如下
if (@available(iOS 13.0, *)) {
[[UITabBarItem appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName:[UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor greenColor];
} else {
return [UIColor redColor];
}
}], NSFontAttributeName:[UIFont fontWithName:fontName size:size]} forState:UIControlStateSelected];
} else {
// Fallback on earlier versions
[[UITabBarItem appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName:[UIColor redColor], NSFontAttributeName:[UIFont fontWithName:fontName size:size]} forState:UIControlStateSelected];
}
现在,我们做完了动图中背景色和文本颜色的适配,接下来我们看看图片如何适配
打开 Assets.xcassets
把图片拖拽进去,我们可以看到这样的页面
然后我们在右侧工具栏中点击最后一栏,点击 Appearances
选择 Any, Dark
,如图所示
我们把 DarkMode 的图片拖进去,如图所示
最后我们加上 ImageView
的代码
[_logoImage setImage:[UIImage imageNamed:@"icon_logo"]];
现在我们就已经完成颜色和图片的 DarkMode 适配,是不是很简单呢 (手动滑稽)
我们可以看到,不管是颜色还是图片,适配都是系统完成的,我们不用关心现在是什么样的样式。
但是在某些场景下,我们可能会有根据当前样式来做一些其他适配的需求,这时我们就需要知道现在什么样式。
我们可以在 UIViewController
或 UIView
中调用 traitCollection.userInterfaceStyle
来获取当前视图的样式,代码如下
if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
[self.titleLabel setText:@"DarkMode"];
}
else {
[self.titleLabel setText:@"LightMode"];
}
那么我们什么时候需要用这样的方法做适配呢,比如说当我们使用 CGColor
的时候,上面说到 UIColor
在 iOS 13 下变成了一个动态颜色,但是 CGColor
仍然只能表示单一的颜色,所以当我们使用到 CGColor
的时候,我们就可以用上面的方法做适配。
颜色
对于 CGColor
我们还有还有另一种适配方法,代码如下
let resolvedColor = labelColor.resolvedColor(with: traitCollection)
layer.borderColor = resolvedColor.cgColor
resolvedColor
方法会根据传递进去的 traitCollection
返回对应的颜色。
图片
对于 UIImage
我们也有类似的方法,代码如下
let image = UIImage(named: "icon")let resovledImage = image?.imageAsset?.image(with: traitCollection)
上面我们说了如何获取当前模式,但是我们要搭配监听方法一起使用,当 light dark 模式切换的时候,要把上面的代码再执行一遍。系统为我们提供了一个回调方法,当 light dark 切换时就会触发这个方法。
// 注意:参数为变化前的traitCollection
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
// 判断两个UITraitCollection对象是否不同
- (BOOL)hasDifferentColorAppearanceComparedToTraitCollection:(UITraitCollection *)traitCollection;
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
// trait发生了改变
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
// 执行操作
}
}
题外话
如果你觉得这样为 CGColor
做适配很麻烦,那么不妨试试 XYColor这个框架。
我们可以看到在动图中是直接改系统的模式,从而让 App 的模式修改,但是对于某些有夜间模式功能的 App 来说,如果用户打开了夜间模式,那么即使现在系统是 light 模式,也要强制用 dark 模式。
我们可以用以下代码将当前 UIViewController
或 UIView
的模式。
overrideUserInterfaceStyle = .darkprint(traitCollection.userInterfaceStyle) // dark
我们可以看到设置了 overrideUserInterfaceStyle
之后,traitCollection.userInterfaceStyle
就是我们设置后的模式了。
需要给每一个 Controller 和 View 都设置一遍吗
答案是不需要,我们先来看一张图。
当我们设置一个 controller 为 dark 之后,这个 controller 下的 view,都会是 dark mode,但是后续推出的 controller 仍然是跟随系统的样式。
因为苹果对 overrideUserInterfaceStyle
属性的解释是这样的。
当我们在一个普通的 controlle, view 上重写这个属性,只会影响当前的视图,不会影响前面的 controller 和后续推出的 controller。
但是当我们在 window
上设置 overrideUserInterfaceStyle
的时候,就会影响 window
下所有的 controller, view,包括后续推出的 controller。
我们回到刚刚的问题上,如果 App 打开夜间模式,那么很简单我们只需要设置 window
的 overrideUserInterfaceStyle
属性就好了。
题外话:当我们用 Xcode11 创建项目,我们会发现项目结构发生了变化,window
从 AppDelegate
移到 SceneDelegate
中。那么如何获取 SceneDelegate
中的 window
呢,代码如下
// 这里就简单介绍一下,实际项目中,如果是iOS应用这么写没问题,但是对于iPadOS应用还需要判断scene的状态是否激活let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .dark
Status Bar
之前 Status Bar
有两种状态,default
和 lightContent
现在 Status Bar
有三种状态,default
, darkContent
和 lightContent
现在的 darkContent
对应之前的 default
,现在的 default
会根据情况自动选择 darkContent
和 lightContent
UIActivityIndicatorView
之前的 UIActivityIndicatorView
有三种 style
分别为 whiteLarge
, white
和 gray
,现在全部废弃。
增加两种 style
分别为 medium
和 large
,指示器颜色用 color
属性修改。
如何在模式切换时打印日志
在 Arguments
中的 Arguments Passed On Launch
里面添加下面这行命令。
-UITraitCollectionChangeLoggingEnabled YES
以上是 iOS 13 如何适配 Dark Mode 的全部内容,如有错误欢迎指出。
WWDC链接 Implementing Dark Mode on iOS