iOS13 (五)暗黑模式Dark Mode

4,573 阅读9分钟

最近公司业务需求要更换APP主题。最开始是一个地方一个地方去改,而且项目中很多老代码是用xib写的,习惯纯代码编程的我改的很难受。而且以后指不定要再次更改主题。

于是我定义了几个主要颜色的宏,代码中只要是设置颜色的地方就用宏。这样只需要改一次,当要切换主题的时候直接对宏进行更改就行了。

结合已做好的切换主题功能,再加上一个暗黑模式判断,如果当前是暗黑模式就用A套色值,如果不是就用B套色值,这样就实现了暗黑模式的适配了。

.

一、定义的宏:

代码中设置颜色时,都用事先定义好的颜色。(下面个别宏只是我的项目场景中会使用到的,并不适用于所有APP,可自行针对自己的项目定义。有些颜色两种模式下没有变动)

/// 暗黑模式 YES是
#define CKDarkMode @available(iOS 13.0, *) && UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark
// MARK: - 十六进制颜色
#define HexOf(rgbValue) Hex_A(rgbValue,1.0)
#define Hex_A(rgbValue,a) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:a]
// MARK: - 用全局变量设置背景、文字,可以优雅的主题切换 (取全局唯一性的名称,便于维护;最前面的优先级最高)
#define Color_Bg        CKDarkMode?HexOf(0x191C32):HexOf(0xf4f4f4) //背景主题颜色    黑色/白色
#define Color_ContView  CKDarkMode?HexOf(0x1D213B):HexOf(0xffffff) //内容、cell颜色  深蓝色/白色 如果背景和cell颜色一样,就都用这个
#define Color_Title     CKDarkMode?HexOf(0xFFFFFF):HexOf(0x393939) //主文字颜色      白色/黑色
#define Color_Subtitle  CKDarkMode?HexOf(0x999999):HexOf(0x999999) //副文字颜色      浅白色/灰色
#define Color_Green     CKDarkMode?HexOf(0x45C98F):HexOf(0x45C98F) //绿涨           (行情、交易)
#define Color_Red       CKDarkMode?HexOf(0xEF0C47):HexOf(0xEF0C47) //红跌           (行情、交易)
//
#define Color_NavBg     CKDarkMode?HexOf(0x1D213B):HexOf(0xffffff) //导航栏背景颜色
#define Color_NavTitle  CKDarkMode?HexOf(0xFFFFFF):HexOf(0x393939) //导航栏标题颜色
#define Color_TabbarBg  CKDarkMode?HexOf(0x1D213B):HexOf(0xffffff) //标签栏背景颜色
#define Color_Selected  CKDarkMode?HexOf(0x46CA8F):HexOf(0x46CA8F) //绿色 (按钮选中、已认证状态的颜色)
#define Color_Line      CKDarkMode?HexOf(0x191C32):HexOf(0xf4f4f4) //分割线
#define Color_DarkGray  CKDarkMode?HexOf(0x333333):HexOf(0x333333) //深灰色
#define Color_Gray      CKDarkMode?HexOf(0x666666):HexOf(0x666666) //灰色
#define Color_LightGray CKDarkMode?HexOf(0x999999):HexOf(0x999999) //浅灰色
#define Color_InputBg   CKDarkMode?HexOf(0x191C32):HexOf(0xf4f4f4) //输入框背景颜色
#define Color_DarkBlue  CKDarkMode?HexOf(0x191C32):HexOf(0x191C32) //深蓝色 (特殊颜色)
#define Color_HalfTitle CKDarkMode?Hex_A(0x999999, 0.5):Hex_A(0x999999, 0.5);//半透明文字 色值是副标题的一半

如果想关闭暗黑模式,直接设置:

#define CKDarkMode NO

Swift版本:

/// 颜色
extension UIColor {
    /// 判断是否是暗黑模式 YES是
    public static let IsDarkMode = UITraitCollection.current.userInterfaceStyle == UIUserInterfaceStyle.dark
    /// 背景主题颜色     黑色/白色
    public static let Bg =       UIColor.IsDarkMode ? CKColor(0x191C32):CKColor(0xf4f4f4)
    /// 内容、cell颜色  深蓝色/白色 如果背景和cell颜色一样,就都用这个
    public static let ContView = UIColor.IsDarkMode ? CKColor(0x1D213B):CKColor(0xffffff)
    /// 主文字颜色       白色/黑色
    public static let Title =    UIColor.IsDarkMode ? CKColor(0xFFFFFF):CKColor(0x393939)
    /// 副文字颜色     浅白色/灰色
    public static let Subtitle = UIColor.IsDarkMode ? CKColor(0x999999):CKColor(0x999999)
    /// 导航栏背景颜色
    public static let NavBg =    UIColor.IsDarkMode ? CKColor(0x1D213B):CKColor(0xffffff)
    /// 导航栏标题颜色
    public static let NavTitle = UIColor.IsDarkMode ? CKColor(0xFFFFFF):CKColor(0x393939)
    /// 标签栏背景颜色
    public static let TabbarBg = UIColor.IsDarkMode ? CKColor(0x1D213B):CKColor(0xffffff)
    /// 蓝色 (按钮选中、已认证状态的颜色)
    public static let Selected = UIColor.IsDarkMode ? CKColor(0x46CA8F):CKColor(0x46CA8F)
    /// 分割线
    public static let Line =     UIColor.IsDarkMode ? CKColor(0x191C32):CKColor(0xf4f4f4)
}

调用

self.view.backgroundColor = .Bg

.

二、遇到的问题:

1、最开始我是用的self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark去做判断,但是有些类并没有UITraitCollection这个属性,很多地方报错。

解决方案:

改用UITraitCollection.current属性来获取当前App的颜色模式。

2、CGColorRef相关:

bt.layer.borderColor = Color_Selected.CGColor;

报错:

Incompatible operand types ('UIColor * _Nonnull' and 'CGColorRef _Nonnull' (aka 'struct CGColor *'))

解决方案:

UIColor *color = Color_Selected;
bt.layer.borderColor = color.CGColor;

3.每次打开APP都能展示正常的模式;但是如果打开APP后再切换模式,已经加载出来的页面依然会显示切换之前的主题模式。

解决方案:
在页面中添加通知,获取到切换主题的通知后重新刷新一下页面颜色(类似于项目中的国际化通知处理逻辑)

4.项目中个别页面的状态栏是固定的白色,在切换页面的时候会把状态栏切换回主题颜色黑色,但是在暗黑模式下就会有问题,因为暗黑模式下整个APP的状态栏都是白色的,这时不需要切换回黑色。

解决方案:
添加一个UIStatusBarStyle变量记录主题状态栏颜色,这样可以不用在控制器内做太多额外的判断。如果用 @available(iOS 13.0, *) 去做判断,需求变更后还要每个地方都去改动代码。用了这种方式,后面即使更改了主题或者关闭了暗黑模式,也不用一一去改代码;也可以通过上面定义的宏CKDarkMode做判断,关闭暗黑模式时只需把CKDarkMode设置为NO就行

UIStatusBarStyle _themeStatusBarStyle;//记录主题状态栏颜色

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    _themeStatusBarStyle = [UIApplication sharedApplication].statusBarStyle;
    // 设置状态栏颜色为白色
    [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent;
}

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    // 恢复状态栏颜色为主题颜色
    [UIApplication sharedApplication].statusBarStyle = _themeStatusBarStyle;
}

5、使用了宏的地方都会报警告,提示我要做系统版本判断,但是实际上我已经在CKDarkMode中判断过了,系统检测不到:

'currentTraitCollection' is only available on iOS 13.0 or newer

解决方案:使用UIColor扩展。
999+的警告有点影响代码视觉体验,后面应该会改用扩展的方式。如果有更好的解决方案请在下方留言。

.

三、UITraitCollection介绍:

1、在 iOS 13 中,我们可以通过 UITraitCollection 来判断当前系统的模式。UIView 和 UIViewController 、UIScreen、UIWindow 都已经遵从了UITraitEnvironment这个协议,因此这些类都拥有一个叫做 traitCollection的属性,在这些类中,我们可以这样去判断当前 App 的颜色模式:

BOOL isDark = (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark);

2、另外,我们还可以通过 UITraitCollection.current这个属性来获取当前 App 的颜色模式。

3、如果暂时不想开放这个功能,可以先暂时全局关闭暗黑模式:

在 Info.plist 文件中,添加 key 为 User Interface Style,类型为 String,value 设置为 Light (Dark)即可,如果重新打开就把这条设置删除。(这种方式是在APP整个生命周期内关闭了暗黑模式;上面的设置#define CKDarkMode NO只是用代码做了判断并只在用了宏的地方起作用)。

(更新:2023年3月)
如果上面的设置无效,可以试试设置Appearance为:LightUIUserInterfaceStyleLight 1.png

4、在 iOS 13中,UIView、UIViewController 、UIWindow 有了一个 overrideUserInterfaceStyle的新属性,可以覆盖系统的模式。

单个页面或视图关闭暗黑模式,设置 overrideUserInterfaceStyle 为对应的模式,强制限制该视图与其子视图以设置的模式进行展示,不跟随系统模式改变进行改变。

self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;

设置此属性会影响当前view / viewController / window 以及它下面的任何内容。
如果你希望一个子视图监听系统的模式,请将 overrideUserInterfaceStyle 属性设置为.unspecified

.

四、拓展

除了我的这种实现方案,还有其他方案可以适配暗黑模式:

1、UIColor扩展:

+(UIColor *)generateDynamicColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor{
    if (@available(iOS 13.0, *)) {
        UIColor *dyColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) {
                return lightColor;
            }else {
                return darkColor;
            }
        }];
        return dyColor;
    }else{
        return lightColor;
    }
}

问题:
这种写法要在每个使用的地方分别传一个普通模式的颜色和暗黑模式的颜色,不方便维护。

优化:
可以定义几个常用颜色函数,特殊的场景就用上面的方法,这样就不需要在每个地方都控制颜色值了。

+(UIColor *)ContViewColor{
    if (@available(iOS 13.0, *)) {
        UIColor *dyColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                return ColorA;
            }else {
                return ColorB;
            }
        }];
        return dyColor;
    }else{
        return ColorB;
    }
}

2、可以在 Images.xcassets 中定义几种常用颜色,并为颜色再配置一个用于暗黑模式的对应的颜色:

3、在 Images.xcassets 中配置不同模式下的图片,当你设置为暗黑模式后就会自动显示对应的图片

4、启动图:

LaunchScreen.storyboard可以像普通的图片那样针对深色模式设置另外的一张图片

5、layer:

UIColor *resolvedColor = [[UIColor labelColor] resolvedColorWithTraitCollection:self.view.traitCollection];
label.layer.borderColor = resolvedColor.CGColor;

6、UIActivityIndicatorView 的 style:

iOS 13前 的 UIActivityIndicatorViewStyle:

typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) {
    UIActivityIndicatorViewStyleWhiteLarge,
    UIActivityIndicatorViewStyleWhite,
    UIActivityIndicatorViewStyleGray,
};

iOS 13后,由于暗黑模式,上述三个属性都被废弃,建议使用如下 style:

typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) {
    UIActivityIndicatorViewStyleMedium,
    UIActivityIndicatorViewStyleLarge,
    UIActivityIndicatorViewStyleWhiteLarge API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleLarge", 
    UIActivityIndicatorViewStyleWhite API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleMedium",
    UIActivityIndicatorViewStyleGray API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleMedium", 
};

7、Status Bar 的 style :

在 iOS 13 之前,状态栏的样式的枚举值也带有着明显的颜色倾向,UIStatusBarStyleDefault、UIStatusBarStyleLightContent

现在,状态栏的 default 样式会根据当前的模式展示不同的颜色,而原有的 lightContent 样式则新增一个 darkContent 的样式与之对应。

typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
    UIStatusBarStyleDefault      = 0, // Automatically chooses light or dark content based on the user interface style
    UIStatusBarStyleLightContent = 1, // Light content, for use on dark backgrounds
    UIStatusBarStyleDarkContent  = 3, // Dark content, for use on light backgrounds
};

8、SF Symbols

.

注意:

命名时要保证这个名字的全局唯一性,避免和项目中其他命名雷同,这样可以保证全局搜索时搜索到的结果只有你想搜索的内容,便于维护。例如你取名RedColor,会搜索到很多其他没用的信息。这种命名思路也可以用在其他地方。

除了改背景颜色、文字颜色,还需要替换图标、图片,这个需要UI配合切图。


(已适配暗黑模式的APP:手机淘宝、微信读书、QQ音乐、网易云音乐、爱奇艺、知乎、网易新闻、百度贴吧)

参考:

How To Adopt Dark Mode In Your iOS App
DarkMode1
DarkMode2
DarkMode3