iOS13适配深色模式(Dark Mode)的实现

所属分类: 软件编程 / IOS 阅读数: 948
收藏 0 赞 0 分享

好像大概也许是一年前, Mac OS系统发布了深色模式外观, 看着挺刺激, 时至今日用着也还挺爽的

终于, 随着iPhone11等新手机的发售, iOS 13系统也正式发布了, 伴随着手机版的深色模式也出现在了大众视野

我们这些iOS程序猿也有事情做了, 原有项目适配iOS13系统, 适配Dark Mode深色模式

虽然现在并没有要求强制适配Dark Mode, 但是DarK适配却也迫在眉睫

Apps on iOS 13 are expected to support dark mode Use system colors and materials Create your own dynamic colors and images Leverage flexible infrastructure

获取当前模式

提供两种方式设置手机当前外观模式

  • 设置 --> 显示与亮度
  • 控制中心, 长按亮度调节按钮

获取当前模式

我们需要选获取到当前出于什么模式, 在根据不同的模式进行适配, iOS 13中新增了获取当前模式的API

Swift

// 获取当前模式
let currentMode = UITraitCollection.current.userInterfaceStyle
if (currentMode == .dark) {
 print("深色模式")
} else if (currentMode == .light) {
 print("浅色模式")
} else {
 print("未知模式")
}

 
open var userInterfaceStyle: UIUserInterfaceStyle { get } 

// 所有模式
public enum UIUserInterfaceStyle : Int {
 // 未指明的
 case unspecified
 // 浅色模式
 case light
 // 深色模式
 case dark
}

OC语言

if (@available(iOS 13.0, *)) {
 UIUserInterfaceStyle mode = UITraitCollection.currentTraitCollection.userInterfaceStyle;
 if (mode == UIUserInterfaceStyleDark) {
  NSLog(@"深色模式");
 } else if (mode == UIUserInterfaceStyleLight) {
  NSLog(@"浅色模式");
 } else {
  NSLog(@"未知模式");
 }
}

// 各种枚举值
typedef NS_ENUM(NSInteger, UIUserInterfaceStyle) {
 UIUserInterfaceStyleUnspecified,
 UIUserInterfaceStyleLight,
 UIUserInterfaceStyleDark,
} API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);

监听系统模式的变化

在iOS13系统中, UIViewController遵循了两个协议: UITraitEnvironmentUIContentContainer协议

UITraitEnvironment协议中, 为我们提供了一个监听当前模式变化的方法

@protocol UITraitEnvironment <NSObject>
// 当前模式
@property (nonatomic, readonly) UITraitCollection *traitCollection API_AVAILABLE(ios(8.0));

// 重写该方法监听模式的改变
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection API_AVAILABLE(ios(8.0));
@end
public protocol UITraitEnvironment : NSObjectProtocol {
 // 当前模式
 @available(iOS 8.0, *)
 var traitCollection: UITraitCollection { get }

 // 重写该方法监听模式的改变
 @available(iOS 8.0, *)
 func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
}


// 使用方法
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
 super.traitCollectionDidChange(previousTraitCollection)
 
 // 每次模式改变的时候, 这里都会执行
 print("模式改变了")
}

颜色相关适配

  • 不同模式的适配主要涉及颜色和图片两个方面的适配
  • 其中颜色适配, 包括相关背景色和字体颜色
  • 当系统模式切换的时候, 我们不需要如何操作, 系统会自动渲染页面, 只需要做好不同模式的颜色和图片即可

UIColor

iOS13之前UIColor只能表示一种颜色,从iOS13开始UIColor是一个动态的颜色,在不同模式下可以分别代表不同的颜色

下面是iOS13系统提供的动态颜色种类, 使用以下颜色值, 在模式切换时, 则不需要做特殊处理

@interface UIColor (UIColorSystemColors)
#pragma mark System colors

@property (class, nonatomic, readonly) UIColor *systemRedColor   API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemGreenColor  API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemBlueColor   API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemOrangeColor  API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemYellowColor  API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemPinkColor   API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemPurpleColor  API_AVAILABLE(ios(9.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemTealColor   API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemIndigoColor  API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
// 灰色种类, 在Light模式下, systemGray6Color更趋向于白色
@property (class, nonatomic, readonly) UIColor *systemGrayColor   API_AVAILABLE(ios(7.0), tvos(9.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);

#pragma mark Foreground colors
@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);

#pragma mark Background colors
@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);

#pragma mark Fill colors
@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);

#pragma mark Other colors
// 这两个是非动态颜色值
@property(class, nonatomic, readonly) UIColor *lightTextColor API_UNAVAILABLE(tvos); // for a dark background
@property(class, nonatomic, readonly) UIColor *darkTextColor API_UNAVAILABLE(tvos);  // for a light background

@property(class, nonatomic, readonly) UIColor *groupTableViewBackgroundColor API_DEPRECATED_WITH_REPLACEMENT("systemGroupedBackgroundColor", ios(2.0, 13.0), tvos(13.0, 13.0));
@property(class, nonatomic, readonly) UIColor *viewFlipsideBackgroundColor API_DEPRECATED("", ios(2.0, 7.0)) API_UNAVAILABLE(tvos);
@property(class, nonatomic, readonly) UIColor *scrollViewTexturedBackgroundColor API_DEPRECATED("", ios(3.2, 7.0)) API_UNAVAILABLE(tvos);
@property(class, nonatomic, readonly) UIColor *underPageBackgroundColor API_DEPRECATED("", ios(5.0, 7.0)) API_UNAVAILABLE(tvos);

@end

上面系统提供的这些颜色种类, 根本不能满足我们正常开发的需要, 大部分的颜色值也都是自定义

系统也为我们提供了创建自定义颜色的方法

@available(iOS 13.0, *)
public init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)

在OC中

+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *traitCollection))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
- (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *traitCollection))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
  • 上面的方法接受一个闭包(block)
  • 当系统在LightMode和DarkMode之间相互切换时就会自动触发此回调
  • 回调返回一个UITraitCollection, 可根据改对象判断是那种模式
fileprivate func getColor() -> UIColor {
 return UIColor { (collection) -> UIColor in
  if (collection.userInterfaceStyle == .dark) {
   return UIColor.red
  }
  return UIColor.green
 }
}

除了上述两个方法之外, UIColor还增加了一个实例方法

// 通过当前traitCollection得到对应UIColor
@available(iOS 13.0, *)
open func resolvedColor(with traitCollection: UITraitCollection) -> UIColor

CGColor

  • UIColor只是设置背景色和文字颜色的类, 可以动态的设置
  • 可是如果是需要设置类似边框颜色等属性时, 又该如何处理呢
  • 设置上述边框属性, 需要用到CGColor类, 但是在iOS13中CGColor并不是动态颜色值, 只能表示一种颜色
  • 在监听模式改变的方法中traitCollectionDidChange, 根据不同的模式进行处理
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
 super.traitCollectionDidChange(previousTraitCollection)
 
 // 每次模式改变的时候, 这里都会执行
 if (previousTraitCollection?.userInterfaceStyle == .dark) {
  redView.layer.borderColor = UIColor.red.cgColor
 } else {
  redView.layer.borderColor = UIColor.green.cgColor
 }
}

图片适配

在iOS中, 图片基本都是放在Assets.xcassets里面, 所以图片的适配, 我们就相对麻烦一些了
正常情况下都是下面这中处理方式

需要适配不同模式的情况下, 需要两套不同的图片, 并做如下设置

在设置Appearances时, 我们选择Any, Dark就可以了(只需要适配深色模式和非深色模式)

适配相关

当前页面模式

原项目中如果没有适配Dark Mode, 当你切换到Dark Mode后, 你可能会发现, 有些部分页面的颜色自动适配了
未设置过背景颜色或者文字颜色的组件, 在Dark Mode模式下, 就是黑色的
这里我们就需要真对该单独App强制设置成Light Mode模式

// 设置改属性, 只会影响当前的视图, 不会影响前面的controller和后续present的controller
@available(iOS 13.0, *)
open var overrideUserInterfaceStyle: UIUserInterfaceStyle

// 使用示例
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
 super.traitCollectionDidChange(previousTraitCollection)
 
 // 每次模式改变的时候, 这里都会执行
 if (previousTraitCollection?.userInterfaceStyle == .dark) {
  // 在Dark模式下, 强制改成Light模式
  overrideUserInterfaceStyle = .light
 }
}

强制项目的显示模式

上面这种方式只能针对某一个页面修改, 如果需要对整个项目禁用Dark模式

可以通过修改window的overrideUserInterfaceStyle属性

在Xcode11创建的项目中, window从AppDelegate移到SceneDelegate中, 添加下面这段代码, 就会做到全局修改显示模式

let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .light

在之前的项目中, 可以在AppDelegate设置如下代码

window.overrideUserInterfaceStyle = .light

我创建的简单项目, 上述代码的确会强制改变当前的模式, 但是状态栏的显示不会被修改, 不知道是不是漏了什么

终极方案

需要在info.plist文件中添加User Interface Style配置, 并设置为Light

<key>UIUserInterfaceStyle</key>
<string>Light</string>

问题又来了, 即使做了上面的修改, 在React Native中, 状态栏的依然是根据不同的模式显示不同的颜色, 该如何处理嘞?

Status Bar更新

在iOS13中苹果对于Status Bar也做了部分修改, 在iOS13之前

public enum UIStatusBarStyle : Int {
 case `default` // 默认文字黑色

 @available(iOS 7.0, *)
 case lightContent // 文字白色
}

从iOS13开始UIStatusBarStyle一共有三种状态

public enum UIStatusBarStyle : Int {
 case `default` // 自动选择黑色或白色

 @available(iOS 7.0, *)
 case lightContent // 文字白色
 
 @available(iOS 13.0, *)
 case darkContent // 文字黑色
}

在React Native的代码中, 设置状态栏的颜色为黑色, 代码如下

<StatusBar barStyle={'dark-content'} />

上面这段代码在iOS13系统的手机中是无效的

虽然上面的代码中设置了dark-content模式, 但是在iOS原生代码中dark-content实际是UIStatusBarStyleDefault

在文件RCTStatusBarManager.m中

RCT_ENUM_CONVERTER(UIStatusBarStyle, (@{
 @"default": @(UIStatusBarStyleDefault),
 @"light-content": @(UIStatusBarStyleLightContent),
 @"dark-content": @(UIStatusBarStyleDefault),
}), UIStatusBarStyleDefault, integerValue);

修改上面代码即可

@"dark-content": @(@available(iOS 13.0, *) ? UIStatusBarStyleDarkContent : UIStatusBarStyleDefault),

iOS13 其他更新

苹果登录

Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.

如果APP支持三方登陆(Facbook、Google、微信、QQ、支付宝等),就必须支持苹果登陆,且要放前边
至于Apple登录按钮的样式, 建议支持使用Apple提供的按钮样式,已经适配各类设备, 可参考Sign In with Apple

LaunchImage

即将被废弃的LaunchImage

  • 从iOS 8的时候,苹果就引入了LaunchScreen,我们可以设置LaunchScreen来作为启动页。
  • 现在你还可以使用LaunchImage来设置启动图, 但是随着苹果设备尺寸越来越多, 适配显然相对麻烦一些
  • 使用LaunchScreen的话,情况会变的很简单,LaunchScreen是支持AutoLayout和SizeClass的,所以适配各种屏幕都不在话下。
  • ⚠️从2020年4月开始,所有App将必须提供LaunchScreen,而LaunchImage即将退出历史舞台

UIWebView

'UIWebView' was deprecated in iOS 12.0: No longer supported; please adopt WKWebView.

从iOS 13开始也不再支持UIWebView控件了, 尽快替换成WKWebView吧

@available(iOS, introduced: 2.0, deprecated: 12.0, message: "No longer supported; please adopt WKWebView.")
open class UIWebView : UIView, NSCoding, UIScrollViewDelegate { }
更多精彩内容其他人还在看

iOS 仿百度外卖-首页重力感应的实例

这篇文章主要介绍了iOS 仿百度外卖-首页重力感应的实例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
收藏 0 赞 0 分享

iOS实现时间显示几分钟前,几小时前以及刚刚的方法示例

这篇文章主要介绍了如何利用iOS实现时间显示是在几小时前,几分钟前以及刚刚的格式,类似大家在qq空间和朋友圈微博看到的效果,文中给出了详细的示例代码,有需要的朋友们可以参考借鉴,下面来一起学习学习吧。
收藏 0 赞 0 分享

iOS 条码及二维码扫描(从相册中读取条形码/二维码)及扫码过程中遇到的坑

本文主要给大家介绍ios中从手机相册中读取条形码和二维码的问题及解决办法,需要的朋友参考下
收藏 0 赞 0 分享

IOS Cache设计详细介绍及简单示例

这篇文章主要介绍了IOS Cache设计详细介绍及简单示例的相关资料,Cache的目的是为了追求更高的速度体验,Cache的源头是两种数据读取方式在成本和性能上的差异,需要的朋友可以参考下
收藏 0 赞 0 分享

iOS本地动态生成验证码的方法

这篇文章主要介绍了iOS本地动态生成验证码的方法,非常不错,具有参考借鉴价值,需要的朋友可以参考下
收藏 0 赞 0 分享

iOS绘制3D饼图的实现方法

饼图常用于统计学模块。常见的一般为2D饼图,这篇文章主要介绍了iOS绘制3D饼图的实现方法,3D饼图更加立体,用户的好感度也比较高,下面需要的朋友可以参考借鉴,一起来看看吧。
收藏 0 赞 0 分享

谈谈iOS开发之JSON格式数据的生成与解析

JSON格式取代了xml给网络传输带来了很大的便利,本篇文章主要介绍了iOS开发:对象直接转化成JSON详解,具有一定的参考价值,有兴趣的可以了解一下。
收藏 0 赞 0 分享

IOS 身份证校验详细介绍及示例代码

这篇文章主要介绍了IOS 身份证校验详细介绍及示例代码的相关资料,这里对身份校验比较详细,附有简单实例,需要的朋友可以参考下
收藏 0 赞 0 分享

IOS 自定义UICollectionView的头视图或者尾视图UICollectionReusableView

这篇文章主要介绍了IOS 自定义UICollectionView的头视图或者尾视图UICollectionReusableView的相关资料,需要的朋友可以参考下
收藏 0 赞 0 分享

IOS 仿支付宝支付屏幕亮度变化机制

这篇文章主要介绍了IOS 仿支付宝支付屏幕亮度变化机制的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
收藏 0 赞 0 分享
查看更多