深色模式,作为 iOS 13 最大的新特性,从设计者角度,带来设计体系、颜色、材质、系统控件、SF Symbols 等若干方面的新的变化;对于开发者来讲,我们熟知的适配;
– 屏幕尺寸
– 屏幕方向
– View 渲染阶段(指 viewDidLoad 和 viewDidAppear 两个阶段,view 的尺寸是变化的)
– iOS SDK 适配

等适配维度,而后又多了一个维度,
– 外观模式(dark or light)

Dark mode 的适配,我们需要处理图片、背景、填充、文字和分割线的逻辑。首先是设计师对两种外观,必须设计两套不同的颜色方案。当两套方案送到开发者手上的时候,我们也需要根据不同外观应用到不同的颜色。但幸好,开发者可以做一次封装把这层逻辑封装起来,给其他开发者使用时,屏蔽内部逻辑。为了这个目的,iOS 提供了被动式适配——动态颜色;

let backgroundColor = UIColor { (trainCollection) -> UIColor in
    if trainCollection.userInterfaceStyle == .dark {
        return UIColor.black
    } else {
        return UIColor.white
    }
}
view.backgroundColor = backgroundColor

和主动式适配,我们可以在 UIViewController 或 UIView 中调用 traitCollection.userInterfaceStyle 来获取当前视图的样式;

if trainCollection.userInterfaceStyle == .dark {
    // Dark
} else {
    // Light
}

或者使用name assets技术,提前配置,让系统自动切换。
实际使用下来,还是有点麻烦。为了进一步简化适配的代码量,Apple 特意提供了所谓的DynamicColorProvider机制,内置的一些动态颜色。
即一个「字体颜色」,在深色模式当中可以指代白色,而在浅色模式当中可以指代黑色。

textLabel.textColor = UIColor.labelColor

在 iOS Design System 里,对界面的文字、背景、图标做了信息分层,相应的需要不同配色。我们以文本颜色为例,iOS 13 内置了 4 层;
– UIColor.labelColor,
– UIColor.secondaryLabelColor,
– UIColor.tertiaryLabelColor,
– UIColor.quaternaryLabelColor

「文本色」(LabelColor)是一级字色,可以提供最高的对比度,主要用于最为重要的内容元素,如内容的主标题。而「二级文本色」(SecondaryLabelColor)可以用于副标题,「三级文本色」(TertiaryLabelColor)用于输入框占位符文本,「四级文本色」(QuaternaryLabelColor)用于禁用状态的文本。

使用动态颜色是最简洁适配 dark 模式的方法。但是它有问题: labelColor 的色值是多少?dark 是多少, light 是多少?

labelColor 的答案很简单,但是 iOS 一共引入了 24 动态颜色,48 种色值。真正在和我们 App 自身的样式系统合并的时候,设计师和开发者都不知道,内置的动态色和 App 自身相协调?

当我为《9 星浏览器》做适配的时候,我的头都是大,我知道 UIColor.separatorColor 可以自动变色,但是色值和我 App 的样式是否和谐,没有色值让我对比。我尝试从网上找 DynamicColor 的色值大全的时候,一时半会没有找到。所以我自己来做一份。

深色模式下的配色

颜色 #hex rgba
labelColor #FFFFFFFF rgba(255,255,255,1.00)
secondaryLabelColor #EBEBF599 rgba(235,235,245,0.60)
tertiaryLabelColor #EBEBF54C rgba(235,235,245,0.30)
quaternaryLabelColor #EBEBF52D rgba(235,235,245,0.18)
linkColor #0984FFFF rgba(9,132,255,1.00)
placeholderTextColor #EBEBF54C rgba(235,235,245,0.30)
separatorColor #54545899 rgba(84,84,88,0.60)
opaqueSeparatorColor #38383AFF rgba(56,56,58,1.00)
systemBackgroundColor #000000FF rgba(0,0,0,1.00)
secondarySystemBackgroundColor #1C1C1EFF rgba(28,28,30,1.00)
tertiarySystemBackgroundColor #2C2C2EFF rgba(44,44,46,1.00)
systemGroupedBackgroundColor #000000FF rgba(0,0,0,1.00)
secondarySystemGroupedBackgroundColor #1C1C1EFF rgba(28,28,30,1.00)
tertiarySystemGroupedBackgroundColor #2C2C2EFF rgba(44,44,46,1.00)
systemFillColor #7878805B rgba(120,120,128,0.36)
secondarySystemGroupedBackgroundColor #1C1C1EFF rgba(28,28,30,1.00)
tertiarySystemGroupedBackgroundColor #2C2C2EFF rgba(44,44,46,1.00)
systemFillColor #7878805B rgba(120,120,128,0.36)
secondarySystemFillColor #78788051 rgba(120,120,128,0.32)
tertiarySystemFillColor #7676803D rgba(118,118,128,0.24)
quaternarySystemFillColor #7676802D rgba(118,118,128,0.18)
systemRedColor #FF453AFF rgba(255,69,58,1.00)
systemGreenColor #30D158FF rgba(48,209,88,1.00)
systemBlueColor #0A84FFFF rgba(10,132,255,1.00)
systemOrangeColor #FF9F0AFF rgba(255,159,10,1.00)
systemYellowColor #FFD60AFF rgba(255,214,10,1.00)
systemPinkColor #FF375FFF rgba(255,55,95,1.00)
systemPurpleColor #BF5AF2FF rgba(191,90,242,1.00)
systemTealColor #64D2FFFF rgba(100,210,255,1.00)
systemIndigoColor #5E5CE6FF rgba(94,92,230,1.00)
systemGrayColor #8E8E93FF rgba(142,142,147,1.00)
systemGray2Color #636366FF rgba(99,99,102,1.00)
systemGray3Color #48484AFF rgba(72,72,74,1.00)

浅色模式下的配色

颜色 #hex rgba
labelColor #000000FF rgba(0,0,0,1.00)
secondaryLabelColor #3C3C4399 rgba(60,60,67,0.60)
tertiaryLabelColor #3C3C434C rgba(60,60,67,0.30)
quaternaryLabelColor #3C3C432D rgba(60,60,67,0.18)
linkColor #007AFFFF rgba(0,122,255,1.00)
placeholderTextColor #3C3C434C rgba(60,60,67,0.30)
separatorColor #3C3C4349 rgba(60,60,67,0.29)
opaqueSeparatorColor #C6C6C8FF rgba(198,198,200,1.00)
systemBackgroundColor #FFFFFFFF rgba(255,255,255,1.00)
secondarySystemBackgroundColor #F2F2F7FF rgba(242,242,247,1.00)
tertiarySystemBackgroundColor #FFFFFFFF rgba(255,255,255,1.00)
systemGroupedBackgroundColor #F2F2F7FF rgba(242,242,247,1.00)
secondarySystemGroupedBackgroundColor #FFFFFFFF rgba(255,255,255,1.00)
tertiarySystemGroupedBackgroundColor #F2F2F7FF rgba(242,242,247,1.00)
systemFillColor #78788033 rgba(120,120,128,0.20)
secondarySystemGroupedBackgroundColor #FFFFFFFF rgba(255,255,255,1.00)
tertiarySystemGroupedBackgroundColor #F2F2F7FF rgba(242,242,247,1.00)
systemFillColor #78788033 rgba(120,120,128,0.20)
secondarySystemFillColor #78788028 rgba(120,120,128,0.16)
tertiarySystemFillColor #7676801E rgba(118,118,128,0.12)
quaternarySystemFillColor #74748014 rgba(116,116,128,0.08)
systemRedColor #FF3B30FF rgba(255,59,48,1.00)
systemGreenColor #34C759FF rgba(52,199,89,1.00)
systemBlueColor #007AFFFF rgba(0,122,255,1.00)
systemOrangeColor #FF9500FF rgba(255,149,0,1.00)
systemYellowColor #FFCC00FF rgba(255,204,0,1.00)
systemPinkColor #FF2D55FF rgba(255,45,85,1.00)
systemPurpleColor #AF52DEFF rgba(175,82,222,1.00)
systemTealColor #5AC8FAFF rgba(90,200,250,1.00)
systemIndigoColor #5856D6FF rgba(88,86,214,1.00)
systemGrayColor #8E8E93FF rgba(142,142,147,1.00)
systemGray2Color #AEAEB2FF rgba(174,174,178,1.00)

从上面表格里,我们有一些发现;

  1. 动态颜色分为 3 大类:文本颜色,用于最基本的信息前景色;填充色,指小块的区域和文字配合传达信息;大面积的背景色,这些颜色是对比色,辅助主题色的显示,如 tableview 的背景色。
  2. 对于文本颜色,4 级颜色,第二级到第四级的颜色包含了透明度,但命名中没有体现;separatorColor 则有明确的不透明版本 opaqueSeparatorColor ;
  3. 相同颜色的不同模式颜色并不是取相反值(如 labelColor),大部分颜色都是细调,典型的如 linkColor。浅色:rgba(0,122,255,255),深色:rgba(9,132,255,255)
  4. systemRedColor 系列的颜色不受外观模式影响。

结论

我用系统动态颜色适配了一个版本的《9 星浏览器》和远程键盘,隐约感觉比之前的配色那里少了点东西,虽然大部分还是相似的。凌晨 1 点提交审核,躺在床上睡不着,心里一直不爽,最后还是爬起来,把全部内置动态颜色改为自定义,凌晨 3 点重新提交;

if #available(iOS 13.0, *) {
            let backgroundColor = UIColor { (trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .light {
                    return UIColor(hex: "#f3f3f3ff")!
                } else {
                    return UIColor(hex: "#2e2f30ff")!
                }
            }
            tableView.separatorColor = backgroundColor
        } else {
            // Fallback on earlier versions
            tableView.separatorColor = UIColor(hex: "#e3e3e3ff")
        }

是的,对于大部分应用,你需要同时处理 dark mode 适配和 iOS 版本适配;当然你可以写 category 或 extension,把上面代码简化为
swift<br /> tableVew.seperatorColor = UIColor(hex: "#e3e3e3ff", darkHex:"#2e2f30ff")

结论:扔掉 iOS 内置的动态颜色,他们毫无用处,上面的表格只是为了证明 动态颜色是多么鸡肋。

特别提醒

  1. 使用 Any, Dark 的 imageassets 在设置导航栏的自定义 backButton 图标时不好用(可能是我姿势不对),改为

    if #available(iOS 13.0, *) {
    // 先设置,不设置后面的 tint 设置不上
    let navBarAppearance = UINavigationBarAppearance()
    navBarAppearance.setBackIndicatorImage(backImage, transitionMaskImage: backImage)
    navBarAppearance.titleTextAttributes = [.font: UIFont.systemFont(ofSize: 16, weight: .regular)]
    navBarAppearance.shadowImage = UIImage()
    navBarAppearance.shadowColor = .clear
    navigationController?.navigationBar.standardAppearance = navBarAppearance
    //             改文字和 backimage 图片的颜色
    let backgroundColor = UIColor { (trainCollection) -> UIColor in
    if trainCollection.userInterfaceStyle == .light {
    return UIColor.black
    } else {
    return UIColor.white
    }
    }

    navigationController?.navigationBar.tintColor = backgroundColor }

  2. 如何启用 dark 模式。对于主 App 而言,只要用户启用了 dark 模式,你的 App 就是 dark 模式;但是对于像《9 星浏览器》的远程键盘、财务键盘的 dark 模式,则需要宿主 App 也启用 dark 模式适配,如远程键盘在微信里没有深色模式,在 Safari 里有的;

  3. 设置里或者在 xcode 切换外观模式? App 如何响应?对于 ViewController, 我们知道 iOS 13 提供了一个监听

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
        // 适配代码
    }
}

但是对于动态颜色是如何作用的呢?如

text.textColor = UIColor.label

原因:系统触发了 updateDisplay,怎么触发还没搞明白,肯定不是

self.view.setNeedDispay()

触发的。不会触发 layoutSubView,更不会触发 viewDidLoad。
4. 谨慎适配 dark mode,尤其是引用了第三方 UI 组件的 App,他们没升级,你在适配时,会遇到千奇百怪的界面。
5. 上面的表格是自动生成的,如果想要添加新的色值或者改格式,你可以下 ColorForDarkMode 自行修改

参考

  1. https://juejin.im/post/5cf6276be51d455a68490b26
  2. https://www.uisdc.com/ios-13-design
  3. https://github.com/hite/ColorForDarkMode
  4. 外国友人写的 cheatsheet