[绍棠] iOS13 暗黑模式(Dark Mode)适配之OC版

首先我们来看下效果图

效果图.gif

如何适配 DarkMode

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.backgroundColorlabel,代码如下



 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 适配,是不是很简单呢 (手动滑稽)

如何获取当前模式 (Light or Dark)

我们可以看到,不管是颜色还是图片,适配都是系统完成的,我们不用关心现在是什么样的样式。

但是在某些场景下,我们可能会有根据当前样式来做一些其他适配的需求,这时我们就需要知道现在什么样式。

我们可以在 UIViewControllerUIView中调用 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 模式。

我们可以用以下代码将当前 UIViewControllerUIView的模式。

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 打开夜间模式,那么很简单我们只需要设置 windowoverrideUserInterfaceStyle属性就好了。

题外话:当我们用 Xcode11 创建项目,我们会发现项目结构发生了变化,windowAppDelegate移到 SceneDelegate中。那么如何获取 SceneDelegate中的 window呢,代码如下

// 这里就简单介绍一下,实际项目中,如果是iOS应用这么写没问题,但是对于iPadOS应用还需要判断scene的状态是否激活let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .dark

其他内容

Status Bar

之前 Status Bar有两种状态,defaultlightContent

现在 Status Bar有三种状态,default,  darkContentlightContent

现在的 darkContent对应之前的 default,现在的 default会根据情况自动选择 darkContentlightContent

UIActivityIndicatorView

之前的 UIActivityIndicatorView有三种 style分别为 whiteLarge, whitegray现在全部废弃

增加两种 style分别为 mediumlarge,指示器颜色用 color属性修改。

如何在模式切换时打印日志

Arguments中的 Arguments Passed On Launch里面添加下面这行命令。

-UITraitCollectionChangeLoggingEnabled YES


以上是 iOS 13 如何适配 Dark Mode 的全部内容,如有错误欢迎指出。

WWDC链接 Implementing Dark Mode on iOS

发布了179 篇原创文章 · 获赞 24 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/happyshaotang2/article/details/102502913